/* Copyright (c) 2018 Jean-Marc VIGLINO, released under the CeCILL-B license (http://www.cecill.info/). */ import ol_Object from 'ol/Object.js'; import ol_geom_LineString from 'ol/geom/LineString.js'; import ol_geom_Point from 'ol/geom/Point.js'; import ol_Feature from 'ol/Feature.js'; import ol_source_Vector from 'ol/source/Vector.js' import {boundingExtent as ol_extent_boundingExtent} from 'ol/extent.js' import {buffer as ol_extent_buffer} from 'ol/extent.js' import {ol_coordinate_dist2d} from "../geom/GeomUtils.js"; /* Define namespace */ var ol_graph = {}; export {ol_graph}; /** * @classdesc * Compute the shortest paths between nodes in a graph source * The source must only contains LinesString. * * It uses a A* optimisation. * You can overwrite methods to customize the result. * @see https://en.wikipedia.org/wiki/Dijkstras_algorithm * @constructor * @fires calculating * @fires start * @fires finish * @fires pause * @param {any} options * @param {ol/source/Vector} options.source the source for the edges * @param {integer} [options.maxIteration=20000] maximum iterations before a pause event is fired, default 20000 * @param {integer} [options.stepIteration=2000] number of iterations before a calculating event is fired, default 2000 * @param {number} [options.epsilon=1E-6] geometric precision (min distance beetween 2 points), default 1E-6 */ var ol_graph_Dijkstra = class olgraphDijskra extends ol_Object { constructor(options) { options = options || {}; super(); this.source = options.source; this.nodes = new ol_source_Vector(); // Maximum iterations this.maxIteration = options.maxIteration || 20000; this.stepIteration = options.stepIteration || 2000; // A* optimisation this.astar = true; this.candidat = []; this.set('epsilon', options.epsilon || 1E-6); } /** Get the weighting of the edge, for example a speed factor * The function returns a value beetween ]0,1] * - 1 = no weighting * - 0.5 = goes twice more faster on this road * * If no feature is provided you must return the lower weighting you're using * @param {ol/Feature} feature * @return {number} a number beetween 0-1 * @api */ weight( /* feature */) { return 1; } /** Get the edge direction * - 0 : the road is blocked * - 1 : direct way * - -1 : revers way * - 2 : both way * @param {ol/Feature} feature * @return {Number} 0: blocked, 1: direct way, -1: revers way, 2:both way * @api */ direction( /* feature */) { return 2; } /** Calculate the length of an edge * @param {ol/Feature|ol/geom/LineString} geom * @return {number} * @api */ getLength(geom) { if (geom.getGeometry) geom = geom.getGeometry(); return geom.getLength(); } /** Get the nodes source concerned in the calculation * @return {ol/source/Vector} */ getNodeSource() { return this.nodes; } /** Get all features at a coordinate * @param {ol/coordinate} coord * @return {Array
    } */ getEdges(coord) { var extent = ol_extent_buffer(ol_extent_boundingExtent([coord]), this.get('epsilon')); var result = []; this.source.forEachFeatureIntersectingExtent(extent, function (f) { result.push(f); }); return result; } /** Get a node at a coordinate * @param {ol/coordinate} coord * @return {ol/Feature} the node */ getNode(coord) { var extent = ol_extent_buffer(ol_extent_boundingExtent([coord]), this.get('epsilon')); var result = []; this.nodes.forEachFeatureIntersectingExtent(extent, function (f) { result.push(f); }); return result[0]; } /** Add a node * @param {ol/coorindate} p * @param {number} wdist the distance to reach this node * @param {ol/Feature} from the feature used to come to this node * @param {ol/Feature} prev the previous node * @return {ol/Feature} the node * @private */ addNode(p, wdist, dist, from, prev) { // Final condition if (this.wdist && wdist > this.wdist) return false; // Look for existing point var node = this.getNode(p); // Optimisation ? var dtotal = wdist + this.getLength(new ol_geom_LineString([this.end, p])) * this.weight(); if (this.astar && this.wdist && dtotal > this.wdist) return false; if (node) { // Allready there if (node !== this.arrival && node.get('wdist') <= wdist) return node; // New candidat node.set('dist', dist); node.set('wdist', wdist); node.set('dtotal', dtotal); node.set('from', from); node.set('prev', prev); if (node === this.arrival) { this.wdist = wdist; } this.candidat.push(node); } else { // New candidat node = new ol_Feature({ geometry: new ol_geom_Point(p), from: from, prev: prev, dist: dist || 0, wdist: wdist, dtotal: dtotal, }); if (wdist < 0) { node.set('wdist', false); } else this.candidat.push(node); // Add it in the node source this.nodes.addFeature(node); } return node; } /** Get the closest coordinate of a node in the graph source (an edge extremity) * @param {ol/coordinate} p * @return {ol/coordinate} * @private */ closestCoordinate(p) { var e = this.source.getClosestFeatureToCoordinate(p); var p0 = e.getGeometry().getFirstCoordinate(); var p1 = e.getGeometry().getLastCoordinate(); if (ol_coordinate_dist2d(p, p0) < ol_coordinate_dist2d(p, p1)) return p0; else return p1; } /** Calculate a path beetween 2 points * @param {ol/coordinate} start * @param {ol/coordinate} end * @return {boolean|Array
      } false if don't start (still running) or start and end nodes */ path(start, end) { if (this.running) return false; // Starting nodes start = this.closestCoordinate(start); this.end = this.closestCoordinate(end); if (start[0] === this.end[0] && start[1] === this.end[1]) { this.dispatchEvent({ type: 'finish', route: [], wDistance: -1, distance: this.wdist }); return false; } // Initialize var self = this; this.nodes.clear(); this.candidat = []; this.wdist = 0; this.running = true; // Starting point this.addNode(start, 0); // Arrival this.arrival = this.addNode(this.end, -1); // Start this.nb = 0; this.dispatchEvent({ type: 'start' }); setTimeout(function () { self._resume(); }); return [start, this.end]; } /** Restart after pause */ resume() { if (this.running) return; if (this.candidat.length) { this.running = true; this.nb = 0; this._resume(); } } /** Pause */ pause() { if (!this.running) return; this.nb = -1; } /** Get the current 'best way'. * This may be used to animate while calculating. * @return {Array
        } */ getBestWay() { var node, max = -1; for (var i = 0, n; n = this.candidat[i]; i++) { if (n.get('wdist') > max) { node = n; max = n.get('wdist'); } } // Calculate route to this node return this.getRoute(node); } /** Go on searching new candidats * @private */ _resume() { if (!this.running) return; while (this.candidat.length) { // Sort by wdist this.candidat.sort(function (a, b) { return (a.get('dtotal') < b.get('dtotal') ? 1 : a.get('dtotal') === b.get('dtotal') ? 0 : -1); }); // First candidate var node = this.candidat.pop(); var p = node.getGeometry().getCoordinates(); // Find connected edges var edges = this.getEdges(p); for (var i = 0, e; e = edges[i]; i++) { if (node.get('from') !== e) { var dist = this.getLength(e); if (dist < 0) { console.log('distance < 0!'); // continue; } var wdist = node.get('wdist') + dist * this.weight(e); dist = node.get('dist') + dist; var pt1 = e.getGeometry().getFirstCoordinate(); var pt2 = e.getGeometry().getLastCoordinate(); var sens = this.direction(e); if (sens !== 0) { if (p[0] === pt1[0] && p[1] === pt1[1] && sens !== -1) { this.addNode(pt2, wdist, dist, e, node); } if (p[0] === pt2[0] && p[0] === pt2[0] && sens !== 1) { this.addNode(pt1, wdist, dist, e, node); } } } // Test overflow or pause if (this.nb === -1 || this.nb++ > this.maxIteration) { this.running = false; this.dispatchEvent({ type: 'pause', overflow: (this.nb !== -1) }); return; } // Take time to do something if (!(this.nb % this.stepIteration)) { var self = this; window.setTimeout(function () { self._resume(); }, 5); this.dispatchEvent({ type: 'calculating' }); return; } } } // Finish! this.nodes.clear(); this.running = false; this.dispatchEvent({ type: 'finish', route: this.getRoute(this.arrival), wDistance: this.wdist, distance: this.arrival.get('dist') }); } /** Get the route to a node * @param {ol/Feature} node * @return {Array
          } * @private */ getRoute(node) { var route = []; while (node) { route.unshift(node.get('from')); node = node.get('prev'); } route.shift(); return route; } } // Typo error for compatibility purposes (to be removed) var ol_graph_Dijskra = ol_graph_Dijkstra export { ol_graph_Dijskra }; export default ol_graph_Dijkstra;