diff --git a/application/assets/css/images/end.png b/application/assets/css/images/end.png new file mode 100644 index 00000000..1aa52d79 Binary files /dev/null and b/application/assets/css/images/end.png differ diff --git a/application/assets/css/images/flag.png b/application/assets/css/images/flag.png new file mode 100644 index 00000000..7ea8679f Binary files /dev/null and b/application/assets/css/images/flag.png differ diff --git a/application/assets/css/images/start.png b/application/assets/css/images/start.png new file mode 100644 index 00000000..46d29e71 Binary files /dev/null and b/application/assets/css/images/start.png differ diff --git a/application/assets/css/main.css b/application/assets/css/main.css index 9d927dc0..3951c0bd 100644 --- a/application/assets/css/main.css +++ b/application/assets/css/main.css @@ -695,6 +695,10 @@ div#finder div#question { opacity: 0; } +#routing-container { + display: block; +} + div.panel { display: none; margin: 30px 0px 0px 0px; diff --git a/application/assets/js/GeometryUtil.js b/application/assets/js/GeometryUtil.js new file mode 100644 index 00000000..2689f944 --- /dev/null +++ b/application/assets/js/GeometryUtil.js @@ -0,0 +1,827 @@ +// Packaging/modules magic dance. +(function (factory) { + var L; + if (typeof define === "function" && define.amd) { + // AMD + define(["leaflet"], factory); + } else if (typeof module !== "undefined") { + // Node/CommonJS + L = require("leaflet"); + module.exports = factory(L); + } else { + // Browser globals + if (typeof window.L === "undefined") throw "Leaflet must be loaded first"; + factory(window.L); + } +})(function (L) { + "use strict"; + + L.Polyline._flat = + L.LineUtil.isFlat || + L.Polyline._flat || + function (latlngs) { + // true if it's a flat array of latlngs; false if nested + return ( + !L.Util.isArray(latlngs[0]) || + (typeof latlngs[0][0] !== "object" && + typeof latlngs[0][0] !== "undefined") + ); + }; + + /** + * @fileOverview Leaflet Geometry utilities for distances and linear referencing. + * @name L.GeometryUtil + */ + + L.GeometryUtil = L.extend(L.GeometryUtil || {}, { + /** + Shortcut function for planar distance between two {L.LatLng} at current zoom. + + @tutorial distance-length + + @param {L.Map} map Leaflet map to be used for this method + @param {L.LatLng} latlngA geographical point A + @param {L.LatLng} latlngB geographical point B + @returns {Number} planar distance + */ + distance: function (map, latlngA, latlngB) { + return map + .latLngToLayerPoint(latlngA) + .distanceTo(map.latLngToLayerPoint(latlngB)); + }, + + /** + Shortcut function for planar distance between a {L.LatLng} and a segment (A-B). + @param {L.Map} map Leaflet map to be used for this method + @param {L.LatLng} latlng - The position to search + @param {L.LatLng} latlngA geographical point A of the segment + @param {L.LatLng} latlngB geographical point B of the segment + @returns {Number} planar distance + */ + distanceSegment: function (map, latlng, latlngA, latlngB) { + var p = map.latLngToLayerPoint(latlng), + p1 = map.latLngToLayerPoint(latlngA), + p2 = map.latLngToLayerPoint(latlngB); + return L.LineUtil.pointToSegmentDistance(p, p1, p2); + }, + + /** + Shortcut function for converting distance to readable distance. + @param {Number} distance distance to be converted + @param {String} unit 'metric' or 'imperial' + @returns {String} in yard or miles + */ + readableDistance: function (distance, unit) { + var isMetric = unit !== "imperial", + distanceStr; + if (isMetric) { + // show metres when distance is < 1km, then show km + if (distance > 1000) { + distanceStr = (distance / 1000).toFixed(2) + " km"; + } else { + distanceStr = distance.toFixed(1) + " m"; + } + } else { + distance *= 1.09361; + if (distance > 1760) { + distanceStr = (distance / 1760).toFixed(2) + " miles"; + } else { + distanceStr = distance.toFixed(1) + " yd"; + } + } + return distanceStr; + }, + + /** + Returns true if the latlng belongs to segment A-B + @param {L.LatLng} latlng - The position to search + @param {L.LatLng} latlngA geographical point A of the segment + @param {L.LatLng} latlngB geographical point B of the segment + @param {?Number} [tolerance=0.2] tolerance to accept if latlng belongs really + @returns {boolean} + */ + belongsSegment: function (latlng, latlngA, latlngB, tolerance) { + tolerance = tolerance === undefined ? 0.2 : tolerance; + var hypotenuse = latlngA.distanceTo(latlngB), + delta = + latlngA.distanceTo(latlng) + latlng.distanceTo(latlngB) - hypotenuse; + return delta / hypotenuse < tolerance; + }, + + /** + * Returns total length of line + * @tutorial distance-length + * + * @param {L.Polyline|Array|Array} coords Set of coordinates + * @returns {Number} Total length (pixels for Point, meters for LatLng) + */ + length: function (coords) { + var accumulated = L.GeometryUtil.accumulatedLengths(coords); + return accumulated.length > 0 ? accumulated[accumulated.length - 1] : 0; + }, + + /** + * Returns a list of accumulated length along a line. + * @param {L.Polyline|Array|Array} coords Set of coordinates + * @returns {Array} Array of accumulated lengths (pixels for Point, meters for LatLng) + */ + accumulatedLengths: function (coords) { + if (typeof coords.getLatLngs == "function") { + coords = coords.getLatLngs(); + } + if (coords.length === 0) return []; + var total = 0, + lengths = [0]; + for (var i = 0, n = coords.length - 1; i < n; i++) { + total += coords[i].distanceTo(coords[i + 1]); + lengths.push(total); + } + return lengths; + }, + + /** + Returns the closest point of a {L.LatLng} on the segment (A-B) + + @tutorial closest + + @param {L.Map} map Leaflet map to be used for this method + @param {L.LatLng} latlng - The position to search + @param {L.LatLng} latlngA geographical point A of the segment + @param {L.LatLng} latlngB geographical point B of the segment + @returns {L.LatLng} Closest geographical point + */ + closestOnSegment: function (map, latlng, latlngA, latlngB) { + var maxzoom = map.getMaxZoom(); + if (maxzoom === Infinity) maxzoom = map.getZoom(); + var p = map.project(latlng, maxzoom), + p1 = map.project(latlngA, maxzoom), + p2 = map.project(latlngB, maxzoom), + closest = L.LineUtil.closestPointOnSegment(p, p1, p2); + return map.unproject(closest, maxzoom); + }, + + /** + Returns the closest latlng on layer. + + Accept nested arrays + + @tutorial closest + + @param {L.Map} map Leaflet map to be used for this method + @param {Array|Array>|L.PolyLine|L.Polygon} layer - Layer that contains the result + @param {L.LatLng} latlng - The position to search + @param {?boolean} [vertices=false] - Whether to restrict to path vertices. + @returns {L.LatLng} Closest geographical point or null if layer param is incorrect + */ + closest: function (map, layer, latlng, vertices) { + var latlngs, + mindist = Infinity, + result = null, + i, + n, + distance, + subResult; + + if (layer instanceof Array) { + // if layer is Array> + if (layer[0] instanceof Array && typeof layer[0][0] !== "number") { + // if we have nested arrays, we calc the closest for each array + // recursive + for (i = 0; i < layer.length; i++) { + subResult = L.GeometryUtil.closest(map, layer[i], latlng, vertices); + if (subResult && subResult.distance < mindist) { + mindist = subResult.distance; + result = subResult; + } + } + return result; + } else if ( + layer[0] instanceof L.LatLng || + typeof layer[0][0] === "number" || + typeof layer[0].lat === "number" + ) { + // we could have a latlng as [x,y] with x & y numbers or {lat, lng} + layer = L.polyline(layer); + } else { + return result; + } + } + + // if we don't have here a Polyline, that means layer is incorrect + // see https://github.com/makinacorpus/Leaflet.GeometryUtil/issues/23 + if (!(layer instanceof L.Polyline)) return result; + + // deep copy of latlngs + latlngs = JSON.parse(JSON.stringify(layer.getLatLngs().slice(0))); + + // add the last segment for L.Polygon + if (layer instanceof L.Polygon) { + // add the last segment for each child that is a nested array + var addLastSegment = function (latlngs) { + if (L.Polyline._flat(latlngs)) { + latlngs.push(latlngs[0]); + } else { + for (var i = 0; i < latlngs.length; i++) { + addLastSegment(latlngs[i]); + } + } + }; + addLastSegment(latlngs); + } + + // we have a multi polygon / multi polyline / polygon with holes + // use recursive to explore and return the good result + if (!L.Polyline._flat(latlngs)) { + for (i = 0; i < latlngs.length; i++) { + // if we are at the lower level, and if we have a L.Polygon, we add the last segment + subResult = L.GeometryUtil.closest(map, latlngs[i], latlng, vertices); + if (subResult.distance < mindist) { + mindist = subResult.distance; + result = subResult; + } + } + return result; + } else { + // Lookup vertices + if (vertices) { + for (i = 0, n = latlngs.length; i < n; i++) { + var ll = latlngs[i]; + distance = L.GeometryUtil.distance(map, latlng, ll); + if (distance < mindist) { + mindist = distance; + result = ll; + result.distance = distance; + } + } + return result; + } + + // Keep the closest point of all segments + for (i = 0, n = latlngs.length; i < n - 1; i++) { + var latlngA = latlngs[i], + latlngB = latlngs[i + 1]; + distance = L.GeometryUtil.distanceSegment( + map, + latlng, + latlngA, + latlngB + ); + if (distance <= mindist) { + mindist = distance; + result = L.GeometryUtil.closestOnSegment( + map, + latlng, + latlngA, + latlngB + ); + result.distance = distance; + } + } + return result; + } + }, + + /** + Returns the closest layer to latlng among a list of layers. + + @tutorial closest + + @param {L.Map} map Leaflet map to be used for this method + @param {Array} layers Set of layers + @param {L.LatLng} latlng - The position to search + @returns {object} ``{layer, latlng, distance}`` or ``null`` if list is empty; + */ + closestLayer: function (map, layers, latlng) { + var mindist = Infinity, + result = null, + ll = null, + distance = Infinity; + + for (var i = 0, n = layers.length; i < n; i++) { + var layer = layers[i]; + if (layer instanceof L.LayerGroup) { + // recursive + var subResult = L.GeometryUtil.closestLayer( + map, + layer.getLayers(), + latlng + ); + if (subResult.distance < mindist) { + mindist = subResult.distance; + result = subResult; + } + } else { + // Single dimension, snap on points, else snap on closest + if (typeof layer.getLatLng == "function") { + ll = layer.getLatLng(); + distance = L.GeometryUtil.distance(map, latlng, ll); + } else { + ll = L.GeometryUtil.closest(map, layer, latlng); + if (ll) distance = ll.distance; // Can return null if layer has no points. + } + if (distance < mindist) { + mindist = distance; + result = { layer: layer, latlng: ll, distance: distance }; + } + } + } + return result; + }, + + /** + Returns the n closest layers to latlng among a list of input layers. + + @param {L.Map} map - Leaflet map to be used for this method + @param {Array} layers - Set of layers + @param {L.LatLng} latlng - The position to search + @param {?Number} [n=layers.length] - the expected number of output layers. + @returns {Array} an array of objects ``{layer, latlng, distance}`` or ``null`` if the input is invalid (empty list or negative n) + */ + nClosestLayers: function (map, layers, latlng, n) { + n = typeof n === "number" ? n : layers.length; + + if (n < 1 || layers.length < 1) { + return null; + } + + var results = []; + var distance, ll; + + for (var i = 0, m = layers.length; i < m; i++) { + var layer = layers[i]; + if (layer instanceof L.LayerGroup) { + // recursive + var subResult = L.GeometryUtil.closestLayer( + map, + layer.getLayers(), + latlng + ); + results.push(subResult); + } else { + // Single dimension, snap on points, else snap on closest + if (typeof layer.getLatLng == "function") { + ll = layer.getLatLng(); + distance = L.GeometryUtil.distance(map, latlng, ll); + } else { + ll = L.GeometryUtil.closest(map, layer, latlng); + if (ll) distance = ll.distance; // Can return null if layer has no points. + } + results.push({ layer: layer, latlng: ll, distance: distance }); + } + } + + results.sort(function (a, b) { + return a.distance - b.distance; + }); + + if (results.length > n) { + return results.slice(0, n); + } else { + return results; + } + }, + + /** + * Returns all layers within a radius of the given position, in an ascending order of distance. + @param {L.Map} map Leaflet map to be used for this method + @param {Array} layers - A list of layers. + @param {L.LatLng} latlng - The position to search + @param {?Number} [radius=Infinity] - Search radius in pixels + @return {object[]} an array of objects including layer within the radius, closest latlng, and distance + */ + layersWithin: function (map, layers, latlng, radius) { + radius = typeof radius == "number" ? radius : Infinity; + + var results = []; + var ll = null; + var distance = 0; + + for (var i = 0, n = layers.length; i < n; i++) { + var layer = layers[i]; + + if (typeof layer.getLatLng == "function") { + ll = layer.getLatLng(); + distance = L.GeometryUtil.distance(map, latlng, ll); + } else { + ll = L.GeometryUtil.closest(map, layer, latlng); + if (ll) distance = ll.distance; // Can return null if layer has no points. + } + + if (ll && distance < radius) { + results.push({ layer: layer, latlng: ll, distance: distance }); + } + } + + var sortedResults = results.sort(function (a, b) { + return a.distance - b.distance; + }); + + return sortedResults; + }, + + /** + Returns the closest position from specified {LatLng} among specified layers, + with a maximum tolerance in pixels, providing snapping behaviour. + + @tutorial closest + + @param {L.Map} map Leaflet map to be used for this method + @param {Array} layers - A list of layers to snap on. + @param {L.LatLng} latlng - The position to snap + @param {?Number} [tolerance=Infinity] - Maximum number of pixels. + @param {?boolean} [withVertices=true] - Snap to layers vertices or segment points (not only vertex) + @returns {object} with snapped {LatLng} and snapped {Layer} or null if tolerance exceeded. + */ + closestLayerSnap: function (map, layers, latlng, tolerance, withVertices) { + tolerance = typeof tolerance == "number" ? tolerance : Infinity; + withVertices = typeof withVertices == "boolean" ? withVertices : true; + + var result = L.GeometryUtil.closestLayer(map, layers, latlng); + if (!result || result.distance > tolerance) return null; + + // If snapped layer is linear, try to snap on vertices (extremities and middle points) + if (withVertices && typeof result.layer.getLatLngs == "function") { + var closest = L.GeometryUtil.closest( + map, + result.layer, + result.latlng, + true + ); + if (closest.distance < tolerance) { + result.latlng = closest; + result.distance = L.GeometryUtil.distance(map, closest, latlng); + } + } + return result; + }, + + /** + Returns the Point located on a segment at the specified ratio of the segment length. + @param {L.Point} pA coordinates of point A + @param {L.Point} pB coordinates of point B + @param {Number} the length ratio, expressed as a decimal between 0 and 1, inclusive. + @returns {L.Point} the interpolated point. + */ + interpolateOnPointSegment: function (pA, pB, ratio) { + return L.point( + pA.x * (1 - ratio) + ratio * pB.x, + pA.y * (1 - ratio) + ratio * pB.y + ); + }, + + /** + Returns the coordinate of the point located on a line at the specified ratio of the line length. + @param {L.Map} map Leaflet map to be used for this method + @param {Array|L.PolyLine} latlngs Set of geographical points + @param {Number} ratio the length ratio, expressed as a decimal between 0 and 1, inclusive + @returns {Object} an object with latLng ({LatLng}) and predecessor ({Number}), the index of the preceding vertex in the Polyline + (-1 if the interpolated point is the first vertex) + */ + interpolateOnLine: function (map, latLngs, ratio) { + latLngs = latLngs instanceof L.Polyline ? latLngs.getLatLngs() : latLngs; + var n = latLngs.length; + if (n < 2) { + return null; + } + + // ensure the ratio is between 0 and 1; + ratio = Math.max(Math.min(ratio, 1), 0); + + if (ratio === 0) { + return { + latLng: + latLngs[0] instanceof L.LatLng ? latLngs[0] : L.latLng(latLngs[0]), + predecessor: -1, + }; + } + if (ratio == 1) { + return { + latLng: + latLngs[latLngs.length - 1] instanceof L.LatLng + ? latLngs[latLngs.length - 1] + : L.latLng(latLngs[latLngs.length - 1]), + predecessor: latLngs.length - 2, + }; + } + + // project the LatLngs as Points, + // and compute total planar length of the line at max precision + var maxzoom = map.getMaxZoom(); + if (maxzoom === Infinity) maxzoom = map.getZoom(); + var pts = []; + var lineLength = 0; + for (var i = 0; i < n; i++) { + pts[i] = map.project(latLngs[i], maxzoom); + if (i > 0) lineLength += pts[i - 1].distanceTo(pts[i]); + } + + var ratioDist = lineLength * ratio; + + // follow the line segments [ab], adding lengths, + // until we find the segment where the points should lie on + var cumulativeDistanceToA = 0, + cumulativeDistanceToB = 0; + for (var i = 0; cumulativeDistanceToB < ratioDist; i++) { + var pointA = pts[i], + pointB = pts[i + 1]; + + cumulativeDistanceToA = cumulativeDistanceToB; + cumulativeDistanceToB += pointA.distanceTo(pointB); + } + + if (pointA == undefined && pointB == undefined) { + // Happens when line has no length + var pointA = pts[0], + pointB = pts[1], + i = 1; + } + + // compute the ratio relative to the segment [ab] + var segmentRatio = + cumulativeDistanceToB - cumulativeDistanceToA !== 0 + ? (ratioDist - cumulativeDistanceToA) / + (cumulativeDistanceToB - cumulativeDistanceToA) + : 0; + var interpolatedPoint = L.GeometryUtil.interpolateOnPointSegment( + pointA, + pointB, + segmentRatio + ); + return { + latLng: map.unproject(interpolatedPoint, maxzoom), + predecessor: i - 1, + }; + }, + + /** + Returns a float between 0 and 1 representing the location of the + closest point on polyline to the given latlng, as a fraction of total line length. + (opposite of L.GeometryUtil.interpolateOnLine()) + @param {L.Map} map Leaflet map to be used for this method + @param {L.PolyLine} polyline Polyline on which the latlng will be search + @param {L.LatLng} latlng The position to search + @returns {Number} Float between 0 and 1 + */ + locateOnLine: function (map, polyline, latlng) { + var latlngs = polyline.getLatLngs(); + if (latlng.equals(latlngs[0])) return 0.0; + if (latlng.equals(latlngs[latlngs.length - 1])) return 1.0; + + var point = L.GeometryUtil.closest(map, polyline, latlng, false), + lengths = L.GeometryUtil.accumulatedLengths(latlngs), + total_length = lengths[lengths.length - 1], + portion = 0, + found = false; + for (var i = 0, n = latlngs.length - 1; i < n; i++) { + var l1 = latlngs[i], + l2 = latlngs[i + 1]; + portion = lengths[i]; + if (L.GeometryUtil.belongsSegment(point, l1, l2, 0.001)) { + portion += l1.distanceTo(point); + found = true; + break; + } + } + if (!found) { + throw ( + "Could not interpolate " + + latlng.toString() + + " within " + + polyline.toString() + ); + } + return portion / total_length; + }, + + /** + Returns a clone with reversed coordinates. + @param {L.PolyLine} polyline polyline to reverse + @returns {L.PolyLine} polyline reversed + */ + reverse: function (polyline) { + return L.polyline(polyline.getLatLngs().slice(0).reverse()); + }, + + /** + Returns a sub-part of the polyline, from start to end. + If start is superior to end, returns extraction from inverted line. + @param {L.Map} map Leaflet map to be used for this method + @param {L.PolyLine} polyline Polyline on which will be extracted the sub-part + @param {Number} start ratio, expressed as a decimal between 0 and 1, inclusive + @param {Number} end ratio, expressed as a decimal between 0 and 1, inclusive + @returns {Array} new polyline + */ + extract: function (map, polyline, start, end) { + if (start > end) { + return L.GeometryUtil.extract( + map, + L.GeometryUtil.reverse(polyline), + 1.0 - start, + 1.0 - end + ); + } + + // Bound start and end to [0-1] + start = Math.max(Math.min(start, 1), 0); + end = Math.max(Math.min(end, 1), 0); + + var latlngs = polyline.getLatLngs(), + startpoint = L.GeometryUtil.interpolateOnLine(map, polyline, start), + endpoint = L.GeometryUtil.interpolateOnLine(map, polyline, end); + // Return single point if start == end + if (start == end) { + var point = L.GeometryUtil.interpolateOnLine(map, polyline, end); + return [point.latLng]; + } + // Array.slice() works indexes at 0 + if (startpoint.predecessor == -1) startpoint.predecessor = 0; + if (endpoint.predecessor == -1) endpoint.predecessor = 0; + var result = latlngs.slice( + startpoint.predecessor + 1, + endpoint.predecessor + 1 + ); + result.unshift(startpoint.latLng); + result.push(endpoint.latLng); + return result; + }, + + /** + Returns true if first polyline ends where other second starts. + @param {L.PolyLine} polyline First polyline + @param {L.PolyLine} other Second polyline + @returns {bool} + */ + isBefore: function (polyline, other) { + if (!other) return false; + var lla = polyline.getLatLngs(), + llb = other.getLatLngs(); + return lla[lla.length - 1].equals(llb[0]); + }, + + /** + Returns true if first polyline starts where second ends. + @param {L.PolyLine} polyline First polyline + @param {L.PolyLine} other Second polyline + @returns {bool} + */ + isAfter: function (polyline, other) { + if (!other) return false; + var lla = polyline.getLatLngs(), + llb = other.getLatLngs(); + return lla[0].equals(llb[llb.length - 1]); + }, + + /** + Returns true if first polyline starts where second ends or start. + @param {L.PolyLine} polyline First polyline + @param {L.PolyLine} other Second polyline + @returns {bool} + */ + startsAtExtremity: function (polyline, other) { + if (!other) return false; + var lla = polyline.getLatLngs(), + llb = other.getLatLngs(), + start = lla[0]; + return start.equals(llb[0]) || start.equals(llb[llb.length - 1]); + }, + + /** + Returns horizontal angle in degres between two points. + @param {L.Point} a Coordinates of point A + @param {L.Point} b Coordinates of point B + @returns {Number} horizontal angle + */ + computeAngle: function (a, b) { + return (Math.atan2(b.y - a.y, b.x - a.x) * 180) / Math.PI; + }, + + /** + Returns slope (Ax+B) between two points. + @param {L.Point} a Coordinates of point A + @param {L.Point} b Coordinates of point B + @returns {Object} with ``a`` and ``b`` properties. + */ + computeSlope: function (a, b) { + var s = (b.y - a.y) / (b.x - a.x), + o = a.y - s * a.x; + return { "a": s, "b": o }; + }, + + /** + Returns LatLng of rotated point around specified LatLng center. + @param {L.LatLng} latlngPoint: point to rotate + @param {double} angleDeg: angle to rotate in degrees + @param {L.LatLng} latlngCenter: center of rotation + @returns {L.LatLng} rotated point + */ + rotatePoint: function (map, latlngPoint, angleDeg, latlngCenter) { + var maxzoom = map.getMaxZoom(); + if (maxzoom === Infinity) maxzoom = map.getZoom(); + var angleRad = (angleDeg * Math.PI) / 180, + pPoint = map.project(latlngPoint, maxzoom), + pCenter = map.project(latlngCenter, maxzoom), + x2 = + Math.cos(angleRad) * (pPoint.x - pCenter.x) - + Math.sin(angleRad) * (pPoint.y - pCenter.y) + + pCenter.x, + y2 = + Math.sin(angleRad) * (pPoint.x - pCenter.x) + + Math.cos(angleRad) * (pPoint.y - pCenter.y) + + pCenter.y; + return map.unproject(new L.Point(x2, y2), maxzoom); + }, + + /** + Returns the bearing in degrees clockwise from north (0 degrees) + from the first L.LatLng to the second, at the first LatLng + @param {L.LatLng} latlng1: origin point of the bearing + @param {L.LatLng} latlng2: destination point of the bearing + @returns {float} degrees clockwise from north. + */ + bearing: function (latlng1, latlng2) { + var rad = Math.PI / 180, + lat1 = latlng1.lat * rad, + lat2 = latlng2.lat * rad, + lon1 = latlng1.lng * rad, + lon2 = latlng2.lng * rad, + y = Math.sin(lon2 - lon1) * Math.cos(lat2), + x = + Math.cos(lat1) * Math.sin(lat2) - + Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1); + + var bearing = ((Math.atan2(y, x) * 180) / Math.PI + 360) % 360; + return bearing >= 180 ? bearing - 360 : bearing; + }, + + /** + Returns the point that is a distance and heading away from + the given origin point. + @param {L.LatLng} latlng: origin point + @param {float} heading: heading in degrees, clockwise from 0 degrees north. + @param {float} distance: distance in meters + @returns {L.latLng} the destination point. + Many thanks to Chris Veness at http://www.movable-type.co.uk/scripts/latlong.html + for a great reference and examples. + */ + destination: function (latlng, heading, distance) { + heading = (heading + 360) % 360; + var rad = Math.PI / 180, + radInv = 180 / Math.PI, + R = 6378137, // approximation of Earth's radius + lon1 = latlng.lng * rad, + lat1 = latlng.lat * rad, + rheading = heading * rad, + sinLat1 = Math.sin(lat1), + cosLat1 = Math.cos(lat1), + cosDistR = Math.cos(distance / R), + sinDistR = Math.sin(distance / R), + lat2 = Math.asin( + sinLat1 * cosDistR + cosLat1 * sinDistR * Math.cos(rheading) + ), + lon2 = + lon1 + + Math.atan2( + Math.sin(rheading) * sinDistR * cosLat1, + cosDistR - sinLat1 * Math.sin(lat2) + ); + lon2 = lon2 * radInv; + lon2 = lon2 > 180 ? lon2 - 360 : lon2 < -180 ? lon2 + 360 : lon2; + return L.latLng([lat2 * radInv, lon2]); + }, + + /** + Returns the the angle of the given segment and the Equator in degrees, + clockwise from 0 degrees north. + @param {L.Map} map: Leaflet map to be used for this method + @param {L.LatLng} latlngA: geographical point A of the segment + @param {L.LatLng} latlngB: geographical point B of the segment + @returns {Float} the angle in degrees. + */ + angle: function (map, latlngA, latlngB) { + var pointA = map.latLngToContainerPoint(latlngA), + pointB = map.latLngToContainerPoint(latlngB), + angleDeg = + (Math.atan2(pointB.y - pointA.y, pointB.x - pointA.x) * 180) / + Math.PI + + 90; + angleDeg += angleDeg < 0 ? 360 : 0; + return angleDeg; + }, + + /** + Returns a point snaps on the segment and heading away from the given origin point a distance. + @param {L.Map} map: Leaflet map to be used for this method + @param {L.LatLng} latlngA: geographical point A of the segment + @param {L.LatLng} latlngB: geographical point B of the segment + @param {float} distance: distance in meters + @returns {L.latLng} the destination point. + */ + destinationOnSegment: function (map, latlngA, latlngB, distance) { + var angleDeg = L.GeometryUtil.angle(map, latlngA, latlngB), + latlng = L.GeometryUtil.destination(latlngA, angleDeg, distance); + return L.GeometryUtil.closestOnSegment(map, latlng, latlngA, latlngB); + }, + }); + + return L.GeometryUtil; +}); diff --git a/application/assets/js/exportGeoJson.js b/application/assets/js/exportGeoJson.js index 3c7ff0ed..f9e05c72 100644 --- a/application/assets/js/exportGeoJson.js +++ b/application/assets/js/exportGeoJson.js @@ -36,6 +36,11 @@ const geojson = ((_) => { extData = JSON.stringify(e); } + if (type == "routing") { + let e = routing.data; + extData = JSON.stringify(e); + } + if (type == "collection") { let collection = markers_group.toGeoJSON(); let bounds = map.getBounds(); diff --git a/application/assets/js/maps.js b/application/assets/js/maps.js index 4a965143..7d43083d 100644 --- a/application/assets/js/maps.js +++ b/application/assets/js/maps.js @@ -51,6 +51,20 @@ const maps = (() => { html: '
', }); + const start_icon = L.icon({ + iconUrl: "assets/css/images/start.png", + iconSize: [35, 40], + iconAnchor: [15, 40], + className: "start-marker", + }); + + const end_icon = L.icon({ + iconUrl: "assets/css/images/end.png", + iconSize: [35, 40], + iconAnchor: [15, 40], + className: "end-marker", + }); + //caching settings from settings panel let caching_time; @@ -464,6 +478,8 @@ const maps = (() => { water_icon, select_icon, tracking_icon, + start_icon, + end_icon, weather_map, caching_tiles, delete_cache, diff --git a/application/assets/js/module.js b/application/assets/js/module.js index 019e24f2..4b453b8d 100644 --- a/application/assets/js/module.js +++ b/application/assets/js/module.js @@ -117,7 +117,7 @@ const module = (() => { ///////////////////////// /////Load GeoJSON/////////// /////////////////////// - let loadGeoJSON = function (filename) { + let loadGeoJSON = function (filename, callback) { let finder = new Applait.Finder({ type: "sdcard", debugMode: false, @@ -150,8 +150,11 @@ const module = (() => { onEachFeature: function (feature, layer) { if (feature.geometry != "") { let p = feature.geometry.coordinates[0]; - p.reverse(); - map.flyTo(p); + map.flyTo([p[1], p[0]]); + } + //routing data + if (feature.properties.segments[0].steps) { + callback(geojson_data, true); } }, // Marker Icon @@ -239,7 +242,9 @@ const module = (() => { if ( p.options.className != "follow-marker" && - p.options.className != "goal-marker" + p.options.className != "goal-marker" && + p.options.className != "start-marker" && + p.options.className != "end-marker" ) { markers_collection[t].setIcon(maps.default_icon); } @@ -247,10 +252,13 @@ const module = (() => { } //show selected marker + let p = markers_collection[index].getIcon(); if ( p.options.className != "follow-marker" && - p.options.className != "goal-marker" + p.options.className != "goal-marker" && + p.options.className != "start-marker" && + p.options.className != "end-marker" ) { markers_collection[index].setIcon(maps.select_icon); } @@ -273,7 +281,7 @@ const module = (() => { } map.setView(markers_collection[index]._latlng, map.getZoom()); - + status.selected_marker = markers_collection[index]; return markers_collection[index]; }; @@ -748,5 +756,6 @@ const module = (() => { sunrise, loadGPX_data, user_input, + format_ms, }; })(); diff --git a/application/assets/js/route-service.js b/application/assets/js/route-service.js index ac72d716..00e1a437 100644 --- a/application/assets/js/route-service.js +++ b/application/assets/js/route-service.js @@ -4,6 +4,7 @@ const rs = ((_) => { mozSystem: true, "Content-Type": "application/json", }); + xhr.open( "GET", "https://api.openrouteservice.org/v2/directions/" + @@ -24,14 +25,13 @@ const rs = ((_) => { xhr.onload = function () { if (xhr.status == 200) { - callback(JSON.parse(xhr.responseText)); + callback(JSON.parse(xhr.responseText), false); } if (xhr.status == 403) { console.log("access forbidden"); } // analyze HTTP status of the response if (xhr.status != 200) { - console.log(xhr.status); helper.side_toaster("the route could not be loaded.", 2000); } }; @@ -46,15 +46,68 @@ const rs = ((_) => { if (action == "add") { if (type == "start") { routing.start = latlng.lng + "," + latlng.lat; + status.selected_marker.setIcon(maps.start_icon); + routing.start_marker_id = status.selected_marker._leaflet_id; } if (type == "end") { routing.end = latlng.lng + "," + latlng.lat; + status.selected_marker.setIcon(maps.end_icon); + routing.end_marker_id = status.selected_marker._leaflet_id; + } + } + markers_group.eachLayer(function (e) { + e.setIcon(maps.default_icon); + if (e._leaflet_id == routing.start_marker_id) { + e.setIcon(maps.start_icon); + } + if (e._leaflet_id == routing.end_marker_id) { + e.setIcon(maps.end_icon); + } + }); + }; + + let closest_average = []; + + let instructions = function (currentPosition) { + if (routing.active == false) return false; + gps_lock = window.navigator.requestWakeLock("gps"); + + let latlng = [mainmarker.device_lat, mainmarker.device_lng]; + + let k = L.GeometryUtil.closest(map, routing.coordinates, latlng); + routing.closest = L.GeometryUtil.closest(map, routing.coordinates, latlng); + + //notification + if (setting.routing_notification == false) return false; + + closest_average.push(k.distance); + let result = 0; + let sum = 0; + if (closest_average.length > 48) { + closest_average.forEach(function (e) { + sum = sum + e; + }); + + if (closest_average.length > 50) { + closest_average.length = 0; + sum = 0; + result = 0; + } + + result = sum / 40; + + if (result > 1) { + navigator.vibrate([1000, 500, 1000]); + console.log("to far"); + } else { + console.log("okay"); } } }; return { + instructions, request, addPoint, }; diff --git a/application/assets/js/settings.js b/application/assets/js/settings.js index 5ce55e28..a6e8ab04 100644 --- a/application/assets/js/settings.js +++ b/application/assets/js/settings.js @@ -52,7 +52,7 @@ const settings = ((_) => { //change label text let d = document.querySelector("label[for='measurement-ckb']"); - setting.measurement ? (d.innerText = "kilometer") : (d.innerText = "miles"); + setting.measurement ? (d.innerText = "metric") : (d.innerText = "imperial"); document.getElementById(id).parentElement.focus(); }; @@ -86,6 +86,11 @@ const settings = ((_) => { localStorage.getItem("scale") != null ? JSON.parse(localStorage.getItem("scale")) : true, + + routing_notification: + localStorage.getItem("routing_notification") != null + ? JSON.parse(localStorage.getItem("routing_notification")) + : true, tracking_screenlock: localStorage.getItem("tracking_screenlock") != null ? JSON.parse(localStorage.getItem("tracking_screenlock")) @@ -114,6 +119,10 @@ const settings = ((_) => { ? (document.getElementById("crosshair-ckb").checked = true) : (document.getElementById("crosshair-ckb").checked = false); + setting.routing_notification + ? (document.getElementById("routing-notification-ckb").checked = true) + : (document.getElementById("routing-notification-ckb").checked = false); + setting.useOnlyCache ? (document.getElementById("useOnlyCache-ckb").checked = true) : (document.getElementById("useOnlyCache-ckb").checked = false); @@ -143,9 +152,6 @@ const settings = ((_) => { ? (general.measurement_unit = "km") : (general.measurement_unit = "mil"); - ///show / hidde scale - setting.scale ? scale.addTo(map) : scale.remove(); - if (setting.measurement) { document.querySelector("label[for='measurement-ckb']").innerText = "metric"; @@ -165,6 +171,26 @@ const settings = ((_) => { document.getElementById("cache-zoom").value = setting.cache_zoom; document.getElementById("export-path").value = setting.export_path; document.getElementById("osm-tag").value = setting.osm_tag; + + ///show / hidde scale + + if (scale != undefined) scale.remove(); + + if (setting.measurement) { + scale = L.control.scale({ + position: "topright", + metric: true, + imperial: false, + }); + } else { + scale = L.control.scale({ + position: "topright", + metric: false, + imperial: true, + }); + } + + setting.scale ? scale.addTo(map) : scale.remove(); }; let load_settings_from_file = function () { diff --git a/application/index.html b/application/index.html index d51006d0..8d35f320 100644 --- a/application/index.html +++ b/application/index.html @@ -66,7 +66,7 @@ autofocus tabindex="0" /> -
+
@@ -116,7 +116,7 @@
-
+ -
+