From 1d32d97d91e694f790f112abb08fd845c7427f91 Mon Sep 17 00:00:00 2001 From: strukturart Date: Mon, 8 Aug 2022 10:57:58 +0200 Subject: [PATCH] routing --- application/assets/css/images/end.png | Bin 0 -> 9248 bytes application/assets/css/images/flag.png | Bin 0 -> 6737 bytes application/assets/css/images/start.png | Bin 0 -> 9087 bytes application/assets/css/main.css | 4 + application/assets/js/GeometryUtil.js | 827 ++++++++++++++++++++++++ application/assets/js/exportGeoJson.js | 5 + application/assets/js/maps.js | 16 + application/assets/js/module.js | 21 +- application/assets/js/route-service.js | 57 +- application/assets/js/settings.js | 34 +- application/index.html | 93 +-- application/index.js | 165 +++-- build/application.zip | Bin 290806 -> 315886 bytes build/o.map-omnisd.zip | Bin 291196 -> 316276 bytes build/o.map.zip | Bin 290806 -> 315886 bytes 15 files changed, 1123 insertions(+), 99 deletions(-) create mode 100644 application/assets/css/images/end.png create mode 100644 application/assets/css/images/flag.png create mode 100644 application/assets/css/images/start.png create mode 100644 application/assets/js/GeometryUtil.js diff --git a/application/assets/css/images/end.png b/application/assets/css/images/end.png new file mode 100644 index 0000000000000000000000000000000000000000..1aa52d7960ec35ee8843d1c28d34cac1e73c9a12 GIT binary patch literal 9248 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 literal 0 HcmV?d00001 diff --git a/application/assets/css/images/flag.png b/application/assets/css/images/flag.png new file mode 100644 index 0000000000000000000000000000000000000000..7ea8679f88bf17447f89c9f4a8535da62c0d8f5e GIT binary patch literal 6737 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 GKk#2Y|M#W< literal 0 HcmV?d00001 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 @@
-
+ -
+