/*
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;