forked from Open-CT/openct-tasks
549 lines
16 KiB
JavaScript
549 lines
16 KiB
JavaScript
if(typeof(Number.prototype.toRad) === "undefined") {
|
|
Number.prototype.toRad = function() {
|
|
return this * Math.PI / 180;
|
|
}
|
|
}
|
|
|
|
function Map(options) {
|
|
|
|
|
|
var defaults = {
|
|
parent: document.body,
|
|
width: 400,
|
|
height: 400,
|
|
map_lng_left: 0,
|
|
map_lng_right: 0,
|
|
map_lat_top: 0,
|
|
map_lat_bottom: 0,
|
|
unit: 'km',
|
|
|
|
// map2d options
|
|
line_color: {r: 64, g: 64, b: 64},
|
|
line_width: 4,
|
|
background_color: {r: 255, g: 255, b: 255},
|
|
text_color: {r: 255, g: 255, b: 255},
|
|
pin_file: null,
|
|
pin_scale: 0.385,
|
|
map_file: null
|
|
}
|
|
|
|
options = Object.assign({}, defaults, options);
|
|
|
|
|
|
/*
|
|
var options = (function() {
|
|
var res = {}
|
|
for(var k in defaults) {
|
|
res[k] = k in options ? options[k] : defaults[k]
|
|
}
|
|
return res
|
|
})()
|
|
*/
|
|
|
|
|
|
// map renderer
|
|
|
|
function Renderer2D() {
|
|
|
|
|
|
function ImageLoader(src, onLoad) {
|
|
var loaded = false;
|
|
var img = new Image();
|
|
img.src = src;
|
|
img.onload = function() {
|
|
loaded = true;
|
|
onLoad && onLoad();
|
|
}
|
|
img.onerror = function() {
|
|
console.error('Error loading image: ' + src);
|
|
}
|
|
this.get = function() {
|
|
return loaded ? img : null;
|
|
}
|
|
}
|
|
|
|
|
|
function CoordinatesConverter() {
|
|
var map_lat_bottomRad = options.map_lat_bottom.toRad()
|
|
var mapLngDelta = (options.map_lng_right - options.map_lng_left)
|
|
var worldMapWidth = ((options.width / mapLngDelta) * 360) / (2 * Math.PI)
|
|
var mapOffsetY = (worldMapWidth / 2 * Math.log((1 + Math.sin(map_lat_bottomRad)) / (1 - Math.sin(map_lat_bottomRad))))
|
|
|
|
this.x = function(lng) {
|
|
return (lng - options.map_lng_left) * (options.width / mapLngDelta);
|
|
}
|
|
|
|
this.y = function(lat) {
|
|
var latitudeRad = lat.toRad()
|
|
return options.height - ((worldMapWidth / 2 * Math.log((1 + Math.sin(latitudeRad)) / (1 - Math.sin(latitudeRad)))) - mapOffsetY)
|
|
}
|
|
}
|
|
|
|
|
|
function rgba(colors, opacity) {
|
|
return 'rgba(' + colors.r + ',' + colors.g + ',' + colors.b + ',' + opacity + ')';
|
|
}
|
|
|
|
|
|
this.clear = function() {
|
|
var img = images.map.get();
|
|
if(img) {
|
|
context.drawImage(img, 0, 0, options.width, options.height)
|
|
} else {
|
|
context.fillStyle = rgba(options.background_color, 1);
|
|
context.fillRect(0, 0, options.width, options.height)
|
|
}
|
|
}
|
|
|
|
|
|
this.line = function(lng1, lat1, lng2, lat2, opacity) {
|
|
context.lineWidth = options.line_width;
|
|
context.strokeStyle = rgba(options.line_color, opacity);
|
|
context.beginPath();
|
|
context.moveTo(coordinates.x(lng1), coordinates.y(lat1));
|
|
context.lineTo(coordinates.x(lng2), coordinates.y(lat2));
|
|
context.stroke();
|
|
}
|
|
|
|
|
|
this.pin = function(lng, lat, label) {
|
|
label = label.substr(0, 2);
|
|
var x = coordinates.x(lng);
|
|
var y = coordinates.y(lat);
|
|
|
|
var img = images.pin.get();
|
|
var w = options.pin_scale * img.width;
|
|
var h = options.pin_scale * img.height;
|
|
if(img) {
|
|
context.drawImage(img, x - w * 0.5, y - h, w, h);
|
|
}
|
|
context.fillStyle = rgba(options.text_color, 1);
|
|
context.textAlign = 'center';
|
|
context.fillText(label, x, y - h * 0.6)
|
|
}
|
|
|
|
|
|
// init
|
|
var images = {
|
|
map: new ImageLoader(options.map_file, this.clear.bind(this)),
|
|
pin: new ImageLoader(options.pin_file)
|
|
}
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = options.width;
|
|
canvas.height = options.height;
|
|
options.parent.appendChild(canvas);
|
|
var context = canvas.getContext('2d');
|
|
var coordinates = new CoordinatesConverter();
|
|
}
|
|
|
|
|
|
|
|
function Renderer3D() {
|
|
var earth = new Earth3D(options);
|
|
|
|
|
|
this.clear = function() {
|
|
earth.clearPaths();
|
|
earth.clearLabels();
|
|
}
|
|
|
|
this.line = function(lng1, lat1, lng2, lat2, opacity) {
|
|
var p1 = {
|
|
lat: lat1,
|
|
lng: lng1
|
|
}
|
|
var p2 = {
|
|
lat: lat2,
|
|
lng: lng2
|
|
}
|
|
earth.addPath(p1, p2);
|
|
}
|
|
|
|
this.pin = function(lng, lat, label) {
|
|
var p = {
|
|
lat: lat,
|
|
lng: lng,
|
|
text: label
|
|
}
|
|
earth.addLabel(p);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// distance calculator
|
|
function GeoDistance(unit) {
|
|
|
|
function getEarthRadius() {
|
|
var earthRadius = {
|
|
'yards': 6967410,
|
|
'km': 6371,
|
|
'miles': 3959,
|
|
'metres': 6371000,
|
|
'feet': 20902231
|
|
};
|
|
return earthRadius[unit] || earthRadius['km'];
|
|
}
|
|
|
|
var r = getEarthRadius(unit)
|
|
|
|
function deg2rad(deg) {
|
|
return deg * (Math.PI / 180)
|
|
}
|
|
|
|
// haversine formula
|
|
this.getDistance = function(lng1, lat1, lng2, lat2) {
|
|
var dLat = deg2rad(lat2 - lat1);
|
|
var dLon = deg2rad(lng2 - lng1);
|
|
var a =
|
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
|
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
|
|
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
|
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
return r * c;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
// graph for pathfinding
|
|
// https://github.com/mburst/dijkstras-algorithm/blob/master/dijkstras.js
|
|
|
|
function PriorityQueue () {
|
|
this._nodes = [];
|
|
|
|
this.enqueue = function (priority, key) {
|
|
this._nodes.push({key: key, priority: priority });
|
|
this.sort();
|
|
};
|
|
|
|
this.dequeue = function () {
|
|
return this._nodes.shift().key;
|
|
};
|
|
|
|
this.sort = function () {
|
|
this._nodes.sort(function (a, b) {
|
|
return a.priority - b.priority;
|
|
});
|
|
};
|
|
|
|
this.isEmpty = function () {
|
|
return !this._nodes.length;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Pathfinding starts here
|
|
*/
|
|
function Graph() {
|
|
var INFINITY = 1/0;
|
|
this.vertices = {};
|
|
|
|
this.addVertex = function(name, edges){
|
|
this.vertices[name] = edges;
|
|
};
|
|
|
|
this.shortestPath = function (start, finish) {
|
|
var nodes = new PriorityQueue(),
|
|
distances = {},
|
|
previous = {},
|
|
path = [],
|
|
smallest, vertex, neighbor, alt;
|
|
|
|
for(vertex in this.vertices) {
|
|
if(vertex === start) {
|
|
distances[vertex] = 0;
|
|
nodes.enqueue(0, vertex);
|
|
} else {
|
|
distances[vertex] = INFINITY;
|
|
nodes.enqueue(INFINITY, vertex);
|
|
}
|
|
previous[vertex] = null;
|
|
}
|
|
|
|
while(!nodes.isEmpty()) {
|
|
smallest = nodes.dequeue();
|
|
if(smallest === finish) {
|
|
path = [];
|
|
while(previous[smallest]) {
|
|
path.push(smallest);
|
|
smallest = previous[smallest];
|
|
}
|
|
break;
|
|
}
|
|
if(!smallest || distances[smallest] === INFINITY){
|
|
continue;
|
|
}
|
|
for(neighbor in this.vertices[smallest]) {
|
|
alt = distances[smallest] + this.vertices[smallest][neighbor];
|
|
if(alt < distances[neighbor]) {
|
|
distances[neighbor] = alt;
|
|
previous[neighbor] = smallest;
|
|
nodes.enqueue(alt, neighbor);
|
|
}
|
|
}
|
|
}
|
|
|
|
return path;
|
|
};
|
|
}
|
|
|
|
|
|
|
|
// data search
|
|
|
|
this.findCity = function(name) {
|
|
for(var i=0,city; city=this.cities[i]; i++) {
|
|
if(city.name == name) return city;
|
|
}
|
|
throw new Error('City not found');
|
|
}
|
|
|
|
this.findNeighbors = function(name) {
|
|
for(var i=0,row,res=[]; row=this.neighbors[i]; i++) {
|
|
if(row[0] == name) res.push(this.findCity(row[1]));
|
|
if(row[1] == name) res.push(this.findCity(row[0]));
|
|
}
|
|
return res;
|
|
}
|
|
|
|
|
|
|
|
// validation
|
|
|
|
function validateLng(lng) {
|
|
if(isNaN(lng)) {
|
|
throw new Error('Longitude is not a number')
|
|
}
|
|
if(lng < options.map_lng_left || lng > options.map_lng_right) {
|
|
throw new Error('Longitude is outside of the map')
|
|
}
|
|
}
|
|
|
|
function validateLat(lat) {
|
|
if(isNaN(lat)) {
|
|
throw new Error('Latitude is not a number')
|
|
}
|
|
if(lat > options.map_lat_top || lat < options.map_lat_bottom) {
|
|
throw new Error('Latitude is outside of the map')
|
|
}
|
|
}
|
|
|
|
|
|
// interface
|
|
|
|
this.clearMap = function() {
|
|
renderer.clear();
|
|
}
|
|
|
|
|
|
this.addLocation = function(longitude, latitude, label) {
|
|
validateLng(longitude);
|
|
validateLat(latitude);
|
|
renderer.pin(longitude, latitude, label);
|
|
}
|
|
|
|
|
|
this.addRoad = function(longitude1, latitude1, longitude2, latitude2, opacity) {
|
|
validateLng(longitude1);
|
|
validateLat(latitude1);
|
|
validateLng(longitude2);
|
|
validateLat(latitude2);
|
|
opacity = opacity || 1;
|
|
renderer.line(longitude1, latitude1, longitude2, latitude2, opacity);
|
|
}
|
|
|
|
|
|
this.geoDistance = function(longitude1, latitude1, longitude2, latitude2) {
|
|
validateLng(longitude1);
|
|
validateLat(latitude1);
|
|
validateLng(longitude2);
|
|
validateLat(latitude2);
|
|
return geo.getDistance(longitude1, latitude1, longitude2, latitude2);
|
|
}
|
|
|
|
|
|
this.getLatitude = function(cityName) {
|
|
return this.findCity(cityName).lat;
|
|
}
|
|
|
|
|
|
this.getLongitude = function(cityName) {
|
|
return this.findCity(cityName).lng;
|
|
}
|
|
|
|
|
|
this.getNeighbors = function(cityName) {
|
|
return this.findNeighbors(cityName);
|
|
}
|
|
|
|
|
|
this.shortestPath = function(cityName1, cityName2) {
|
|
return graph.shortestPath(cityName1, cityName2).concat(cityName1).reverse();
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// init
|
|
|
|
if(options.map3d) {
|
|
var renderer = new Renderer3D();
|
|
} else {
|
|
var renderer = new Renderer2D();
|
|
}
|
|
|
|
|
|
|
|
var geo = new GeoDistance(options.unit);
|
|
var graph = new Graph();
|
|
|
|
for(var i=0,city1; city1=this.cities[i]; i++) {
|
|
var neighbors = this.findNeighbors(city1.name);
|
|
if(!neighbors.length) {
|
|
console.error(city1.name + ' has no neighbors')
|
|
}
|
|
var edges = {};
|
|
for(var j=0,city2; city2=neighbors[j]; j++) {
|
|
edges[city2.name] = geo.getDistance(city1.lng, city1.lat, city2.lng, city2.lat);
|
|
}
|
|
graph.addVertex(city1.name, edges);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
// data
|
|
|
|
Map.prototype.cities = [
|
|
{ name: "Dunkerque", lat: 51.069360, lng: 2.376571 },
|
|
{ name: "Calais", lat: 50.979622, lng: 1.855583 },
|
|
{ name: "Lille", lat: 50.650582, lng: 3.056121 },
|
|
{ name: "Béthune", lat: 50.545887, lng: 2.648391 },
|
|
{ name: "Lens", lat: 50.381367, lng: 3.056121 },
|
|
{ name: "Valenciennes", lat: 50.366410, lng: 3.531806 },
|
|
{ name: "Amiens", lat: 49.887806, lng: 2.308616 },
|
|
{ name: "Le Havre", lat: 49.483984, lng: 0.134056 },
|
|
{ name: "Rouen", lat: 49.439114, lng: 1.108078 },
|
|
{ name: "Reims", lat: 49.259638, lng: 4.007492 },
|
|
{ name: "Thionville", lat: 49.364333, lng: 6.182052 },
|
|
{ name: "Metz", lat: 49.110074, lng: 6.182052 },
|
|
{ name: "Strasbourg", lat: 48.586601, lng: 7.745017 },
|
|
{ name: "Nancy", lat: 48.691295, lng: 6.204704 },
|
|
{ name: "Paris", lat: 48.855815, lng: 2.353920 },
|
|
{ name: "Caen", lat: 49.169900, lng: -0.386932 },
|
|
{ name: "Troyes", lat: 48.287473, lng: 4.052795 },
|
|
{ name: "Brest", lat: 48.392168, lng: -4.486885 },
|
|
{ name: "Lorient", lat: 47.749043, lng: -3.376953 },
|
|
{ name: "Rennes", lat: 48.107996, lng: -1.678077 },
|
|
{ name: "Le Mans", lat: 48.003301, lng: 0.202011 },
|
|
{ name: "Orléans", lat: 47.913563, lng: 1.900886 },
|
|
{ name: "Tours", lat: 47.405046, lng: 0.700347 },
|
|
{ name: "Angers", lat: 47.479828, lng: -0.568145 },
|
|
{ name: "Nantes", lat: 47.240526, lng: -1.564819 },
|
|
{ name: "Saint-Nazaire", lat: 47.285395, lng: -2.199066 },
|
|
{ name: "Dijon", lat: 47.330264, lng: 5.049469 },
|
|
{ name: "Mulhouse", lat: 47.763999, lng: 7.337287 },
|
|
{ name: "Montbéliard", lat: 47.509741, lng: 6.793647 },
|
|
{ name: "Besançon", lat: 47.270439, lng: 6.023490 },
|
|
{ name: "Annemasse", lat: 46.268361, lng: 6.227355 },
|
|
{ name: "Annecy", lat: 45.969233, lng: 6.159400 },
|
|
{ name: "Chambéry", lat: 45.670105, lng: 5.932884 },
|
|
{ name: "Grenoble", lat: 45.296196, lng: 5.706367 },
|
|
{ name: "Lyon", lat: 45.834626, lng: 4.800300 },
|
|
{ name: "Saint-Etienne", lat: 45.550454, lng: 4.369918 },
|
|
{ name: "Valence", lat: 45.071850, lng: 4.890907 },
|
|
{ name: "Nice", lat: 43.950121, lng: 7.269332 },
|
|
{ name: "Toulon", lat: 43.426648, lng: 5.932884 },
|
|
{ name: "Marseille", lat: 43.591168, lng: 5.343940 },
|
|
{ name: "Avigon", lat: 44.174467, lng: 4.822952 },
|
|
{ name: "Nîmes", lat: 44.084729, lng: 4.347267 },
|
|
{ name: "Montpellier", lat: 43.860383, lng: 3.871582 },
|
|
{ name: "Perpignan", lat: 43.037782, lng: 2.874908 },
|
|
{ name: "Toulouse", lat: 43.860383, lng: 1.425201 },
|
|
{ name: "Pau", lat: 43.591168, lng: -0.364280 },
|
|
{ name: "Bayonne", lat: 43.755688, lng: -1.496864 },
|
|
// real lat:
|
|
// { name: "Bayonne", lat: 43.499387, lng: -1.496864 },
|
|
{ name: "Bordeaux", lat: 44.997068, lng: -0.593449 },
|
|
{ name: "Clermont-Ferrand", lat: 45.879495, lng: 3.078773 },
|
|
{ name: "Limoges", lat: 45.909408, lng: 1.243988 },
|
|
{ name: "Angoulême", lat: 45.744887, lng: 0.156707 },
|
|
{ name: "La Rochelle", lat: 46.238448, lng: -1.157089 },
|
|
{ name: "Poitiers", lat: 46.627314, lng: 0.315269 }
|
|
];
|
|
|
|
|
|
Map.prototype.neighbors = [
|
|
["Brest", "Lorient"],
|
|
["Brest", "Rennes"],
|
|
["Lorient", "Rennes"],
|
|
["Rennes", "Nantes"],
|
|
["Nantes", "Saint-Nazaire"],
|
|
["Rennes", "Le Mans"],
|
|
["Le Mans", "Paris"],
|
|
["Paris", "Orléans"],
|
|
["Le Mans", "Tours"],
|
|
["Orléans", "Limoges"],
|
|
["Le Mans", "Angers"],
|
|
["Nantes", "La Rochelle"],
|
|
["La Rochelle", "Angoulême"],
|
|
["Nantes", "Angoulême"],
|
|
["Angers", "Nantes"],
|
|
["Poitiers", "Angoulême"],
|
|
["Tours", "Poitiers"],
|
|
["Angoulême", "Bordeaux"],
|
|
["Bordeaux", "Bayonne"],
|
|
["Bayonne", "Pau"],
|
|
["Pau", "Toulouse"],
|
|
["Bordeaux", "Toulouse"],
|
|
["Toulouse", "Perpignan"],
|
|
["Toulouse", "Montpellier"],
|
|
["Montpellier", "Nîmes"],
|
|
["Nîmes", "Avigon"],
|
|
["Avigon", "Marseille"],
|
|
["Marseille", "Toulon"],
|
|
["Toulon", "Nice"],
|
|
["Avigon", "Valence"],
|
|
["Valence", "Grenoble"],
|
|
["Grenoble", "Chambéry"],
|
|
["Chambéry", "Annecy"],
|
|
["Annecy", "Annemasse"],
|
|
["Valence", "Saint-Etienne"],
|
|
["Lyon", "Saint-Etienne"],
|
|
["Lyon", "Grenoble"],
|
|
["Clermont-Ferrand", "Saint-Etienne"],
|
|
["Clermont-Ferrand", "Limoges"],
|
|
["Limoges", "Angoulême"],
|
|
["Paris", "Troyes"],
|
|
["Troyes", "Dijon"],
|
|
["Dijon", "Besançon"],
|
|
["Dijon", "Lyon"],
|
|
["Besançon", "Montbéliard"],
|
|
["Montbéliard", "Mulhouse"],
|
|
["Mulhouse", "Strasbourg"],
|
|
["Strasbourg", "Nancy"],
|
|
["Nancy", "Paris"],
|
|
["Troyes", "Nancy"],
|
|
["Nancy", "Metz"],
|
|
["Metz", "Thionville"],
|
|
["Metz", "Reims"],
|
|
["Reims", "Paris"],
|
|
["Paris", "Rouen"],
|
|
["Rouen", "Le Havre"],
|
|
["Caen", "Rennes"],
|
|
["Rouen", "Caen"],
|
|
["Calais", "Dunkerque"],
|
|
["Dunkerque", "Béthune"],
|
|
["Lille", "Béthune"],
|
|
["Béthune", "Lens"],
|
|
["Lens", "Valenciennes"],
|
|
["Lens", "Lille"],
|
|
["Lens", "Paris"],
|
|
["Amiens", "Paris"],
|
|
["Amiens", "Lens"],
|
|
["Reims", "Lens"],
|
|
["Lens", "Lille"]
|
|
]; |