355 lines
9.7 KiB
JavaScript
355 lines
9.7 KiB
JavaScript
/*
|
|
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<ol/Feature>}
|
|
*/
|
|
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<ol/coordinate>} 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<ol/Feature>}
|
|
*/
|
|
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<ol/Feature>}
|
|
* @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; |