2020-07-17 17:17:06 +00:00
|
|
|
|
require('ol/ol.css');
|
|
|
|
|
|
2020-07-17 17:59:34 +00:00
|
|
|
|
const axios = require('axios');
|
2020-07-17 17:17:06 +00:00
|
|
|
|
const {Map, View} = require('ol');
|
|
|
|
|
|
|
|
|
|
const TileLayer = require('ol/layer/Tile').default;
|
|
|
|
|
const XYZSource = require('ol/source/XYZ').default;
|
|
|
|
|
|
|
|
|
|
const VectorLayer = require('ol/layer/Vector').default;
|
|
|
|
|
const VectorSource = require('ol/source/Vector').default;
|
2020-07-23 18:49:01 +00:00
|
|
|
|
const {getVectorContext} = require('ol/render');
|
2020-07-17 17:17:06 +00:00
|
|
|
|
|
|
|
|
|
const Feature = require('ol/Feature').default;
|
|
|
|
|
const Point = require('ol/geom/Point').default;
|
|
|
|
|
const LineString = require('ol/geom/LineString').default;
|
|
|
|
|
|
|
|
|
|
const proj = require('ol/proj');
|
|
|
|
|
|
2020-07-23 21:09:05 +00:00
|
|
|
|
const {Style, Fill, Stroke, Circle, Icon} = require('ol/style');
|
2020-07-17 17:17:06 +00:00
|
|
|
|
const color = require('color');
|
|
|
|
|
|
2020-07-17 17:59:34 +00:00
|
|
|
|
const mapboxToken = `pk.eyJ1IjoibWF0dGVvZGVsYWJyZSIsImEiOiJja2NxaTUyMmUwcmFhMn\
|
|
|
|
|
h0NmFsdzQ3emxqIn0.cyxF0h36emIMTk3cc4VqUw`;
|
2020-07-17 17:17:06 +00:00
|
|
|
|
|
2020-07-23 15:29:35 +00:00
|
|
|
|
const simulation = require('../tam/simulation');
|
2020-07-19 20:13:26 +00:00
|
|
|
|
const network = require('../tam/network.json');
|
2020-07-17 17:17:06 +00:00
|
|
|
|
|
2020-07-22 21:11:00 +00:00
|
|
|
|
const getRouteColors = routes =>
|
|
|
|
|
{
|
|
|
|
|
const colors = routes.filter(
|
|
|
|
|
// Only consider normal routes (excluding alternate routes)
|
|
|
|
|
([lineRef, routeRef]) =>
|
|
|
|
|
network.lines[lineRef].routes[routeRef].state === 'normal'
|
|
|
|
|
).map(([lineRef]) => network.lines[lineRef].color);
|
|
|
|
|
|
|
|
|
|
if (colors.length >= 1)
|
|
|
|
|
{
|
|
|
|
|
return colors;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ['#FFFFFF'];
|
|
|
|
|
};
|
|
|
|
|
|
2020-07-23 15:29:35 +00:00
|
|
|
|
const makeDataSources = () =>
|
2020-07-17 17:17:06 +00:00
|
|
|
|
{
|
2020-07-17 17:59:34 +00:00
|
|
|
|
const segmentsSource = new VectorSource();
|
|
|
|
|
const stopsSource = new VectorSource();
|
2020-07-17 17:17:06 +00:00
|
|
|
|
|
2020-07-17 17:59:34 +00:00
|
|
|
|
segmentsSource.addFeatures(
|
2020-07-22 21:11:00 +00:00
|
|
|
|
Object.values(network.segments).map(({routes, points}) =>
|
2020-07-17 21:48:32 +00:00
|
|
|
|
new Feature({
|
2020-07-22 21:11:00 +00:00
|
|
|
|
colors: getRouteColors(routes),
|
2020-07-17 21:48:32 +00:00
|
|
|
|
geometry: new LineString(points.map(
|
|
|
|
|
({lat, lon}) => proj.fromLonLat([lon, lat])
|
|
|
|
|
)),
|
|
|
|
|
})
|
2020-07-17 17:59:34 +00:00
|
|
|
|
)
|
|
|
|
|
);
|
2020-07-17 17:17:06 +00:00
|
|
|
|
|
2020-07-17 17:59:34 +00:00
|
|
|
|
stopsSource.addFeatures(
|
2020-07-22 21:11:00 +00:00
|
|
|
|
Object.values(network.stops).map(({routes, lon, lat}) =>
|
2020-07-17 17:59:34 +00:00
|
|
|
|
new Feature({
|
2020-07-22 21:11:00 +00:00
|
|
|
|
colors: getRouteColors(routes),
|
2020-07-17 21:48:32 +00:00
|
|
|
|
geometry: new Point(proj.fromLonLat([lon, lat])),
|
2020-07-17 17:59:34 +00:00
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
);
|
2020-07-17 17:17:06 +00:00
|
|
|
|
|
2020-07-17 17:59:34 +00:00
|
|
|
|
return {segmentsSource, stopsSource};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const makeBorderColor = mainColor =>
|
|
|
|
|
{
|
|
|
|
|
const hsl = color(mainColor).hsl();
|
|
|
|
|
hsl.color = Math.max(0, hsl.color[2] -= 20);
|
|
|
|
|
return hsl.hex();
|
2020-07-17 17:17:06 +00:00
|
|
|
|
};
|
|
|
|
|
|
2020-07-23 21:09:05 +00:00
|
|
|
|
const makeCourseColor = mainColor =>
|
|
|
|
|
{
|
|
|
|
|
const hsl = color(mainColor).hsl();
|
|
|
|
|
hsl.color = Math.max(0, hsl.color[2] += 10);
|
|
|
|
|
return hsl.hex();
|
|
|
|
|
};
|
|
|
|
|
|
2020-07-23 23:32:39 +00:00
|
|
|
|
const sizes = {
|
|
|
|
|
segmentOuter: 8,
|
|
|
|
|
segmentInner: 6,
|
|
|
|
|
stopRadius: 6,
|
|
|
|
|
stopBorder: 1.5,
|
|
|
|
|
courseSize: 15,
|
|
|
|
|
courseOuterBorder: 13,
|
|
|
|
|
courseBorder: 10,
|
|
|
|
|
courseInnerBorder: 7,
|
|
|
|
|
};
|
|
|
|
|
|
2020-07-23 18:49:01 +00:00
|
|
|
|
const segmentBorderStyle = feature => new Style({
|
2020-07-17 17:59:34 +00:00
|
|
|
|
stroke: new Stroke({
|
2020-07-17 21:48:32 +00:00
|
|
|
|
color: makeBorderColor(feature.get('colors')[0]),
|
2020-07-23 23:32:39 +00:00
|
|
|
|
width: sizes.segmentOuter,
|
2020-07-17 17:59:34 +00:00
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
2020-07-23 18:49:01 +00:00
|
|
|
|
const segmentInnerStyle = feature => new Style({
|
2020-07-17 17:59:34 +00:00
|
|
|
|
stroke: new Stroke({
|
2020-07-17 21:48:32 +00:00
|
|
|
|
color: feature.get('colors')[0],
|
2020-07-23 23:32:39 +00:00
|
|
|
|
width: sizes.segmentInner,
|
2020-07-17 17:59:34 +00:00
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
2020-07-23 18:49:01 +00:00
|
|
|
|
const stopStyle = feature => new Style({
|
2020-07-17 17:59:34 +00:00
|
|
|
|
image: new Circle({
|
|
|
|
|
fill: new Fill({
|
2020-07-17 21:48:32 +00:00
|
|
|
|
color: feature.get('colors')[0],
|
2020-07-17 17:59:34 +00:00
|
|
|
|
}),
|
|
|
|
|
stroke: new Stroke({
|
2020-07-17 21:48:32 +00:00
|
|
|
|
color: makeBorderColor(feature.get('colors')[0]),
|
2020-07-23 23:32:39 +00:00
|
|
|
|
width: sizes.stopBorder,
|
2020-07-17 17:59:34 +00:00
|
|
|
|
}),
|
2020-07-23 23:32:39 +00:00
|
|
|
|
radius: sizes.stopRadius,
|
2020-07-17 17:59:34 +00:00
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
2020-07-23 21:09:05 +00:00
|
|
|
|
const courseStyles = {};
|
|
|
|
|
|
|
|
|
|
const getCourseStyle = color =>
|
|
|
|
|
{
|
|
|
|
|
if (!(color in courseStyles))
|
|
|
|
|
{
|
|
|
|
|
const icon = document.createElement('canvas');
|
|
|
|
|
|
2020-07-23 23:32:39 +00:00
|
|
|
|
const shapeSize = sizes.courseSize;
|
|
|
|
|
const iconSize = sizes.courseSize + sizes.courseOuterBorder;
|
2020-07-23 21:09:05 +00:00
|
|
|
|
|
|
|
|
|
icon.width = iconSize;
|
2020-07-23 23:32:39 +00:00
|
|
|
|
icon.height = iconSize;
|
2020-07-23 21:09:05 +00:00
|
|
|
|
|
|
|
|
|
const cx = icon.width / 2;
|
|
|
|
|
const cy = icon.height / 2;
|
|
|
|
|
|
|
|
|
|
const iconCtx = icon.getContext('2d');
|
|
|
|
|
|
|
|
|
|
for (let [color, size] of [
|
2020-07-23 23:32:39 +00:00
|
|
|
|
[makeBorderColor(color), sizes.courseOuterBorder],
|
|
|
|
|
[color, sizes.courseBorder],
|
|
|
|
|
[makeCourseColor(color), sizes.courseInnerBorder]
|
2020-07-23 21:09:05 +00:00
|
|
|
|
])
|
|
|
|
|
{
|
|
|
|
|
iconCtx.fillStyle = color;
|
|
|
|
|
iconCtx.strokeStyle = color;
|
|
|
|
|
iconCtx.lineWidth = size;
|
|
|
|
|
iconCtx.lineJoin = 'round';
|
|
|
|
|
iconCtx.miterLimit = 200000;
|
|
|
|
|
|
|
|
|
|
iconCtx.beginPath();
|
|
|
|
|
iconCtx.moveTo(cx - 0.5 * shapeSize, cy - 0.3 * shapeSize);
|
|
|
|
|
iconCtx.lineTo(cx + 0.5 * shapeSize, cy);
|
|
|
|
|
iconCtx.lineTo(cx - 0.5 * shapeSize, cy + 0.3 * shapeSize);
|
|
|
|
|
iconCtx.closePath();
|
|
|
|
|
iconCtx.stroke();
|
|
|
|
|
iconCtx.fill();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
courseStyles[color] = new Style({
|
|
|
|
|
image: new Icon({
|
|
|
|
|
img: icon,
|
|
|
|
|
imgSize: [icon.width, icon.height],
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return courseStyles[color];
|
|
|
|
|
};
|
2020-07-23 18:49:01 +00:00
|
|
|
|
|
2020-07-23 15:29:35 +00:00
|
|
|
|
const createMap = target =>
|
2020-07-17 17:17:06 +00:00
|
|
|
|
{
|
|
|
|
|
// Map background
|
|
|
|
|
const backgroundSource = new XYZSource({
|
|
|
|
|
url: 'https://api.mapbox.com/' + [
|
|
|
|
|
'styles', 'v1', 'mapbox', 'streets-v11',
|
|
|
|
|
'tiles', '512', '{z}', '{x}', '{y}',
|
|
|
|
|
].join('/') + `?access_token=${mapboxToken}`,
|
|
|
|
|
tileSize: [512, 512],
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const backgroundLayer = new TileLayer({
|
|
|
|
|
source: backgroundSource,
|
|
|
|
|
});
|
|
|
|
|
|
2020-07-23 15:29:35 +00:00
|
|
|
|
// Static data overlay
|
|
|
|
|
const {segmentsSource, stopsSource} = makeDataSources();
|
2020-07-17 17:59:34 +00:00
|
|
|
|
|
|
|
|
|
const segmentsBorderLayer = new VectorLayer({
|
|
|
|
|
source: segmentsSource,
|
2020-07-23 18:49:01 +00:00
|
|
|
|
style: segmentBorderStyle,
|
2020-07-17 17:59:34 +00:00
|
|
|
|
|
|
|
|
|
updateWhileInteracting: true,
|
|
|
|
|
updateWhileAnimating: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const segmentsInnerLayer = new VectorLayer({
|
|
|
|
|
source: segmentsSource,
|
2020-07-23 18:49:01 +00:00
|
|
|
|
style: segmentInnerStyle,
|
2020-07-17 17:59:34 +00:00
|
|
|
|
|
|
|
|
|
updateWhileInteracting: true,
|
|
|
|
|
updateWhileAnimating: true,
|
|
|
|
|
});
|
2020-07-17 17:17:06 +00:00
|
|
|
|
|
2020-07-17 17:59:34 +00:00
|
|
|
|
const stopsLayer = new VectorLayer({
|
|
|
|
|
source: stopsSource,
|
2020-07-23 18:49:01 +00:00
|
|
|
|
style: stopStyle,
|
2020-07-17 17:17:06 +00:00
|
|
|
|
|
2020-07-17 17:59:34 +00:00
|
|
|
|
minZoom: 13,
|
2020-07-17 17:17:06 +00:00
|
|
|
|
updateWhileInteracting: true,
|
|
|
|
|
updateWhileAnimating: true,
|
|
|
|
|
});
|
|
|
|
|
|
2020-07-23 18:49:01 +00:00
|
|
|
|
// Setup map
|
|
|
|
|
const view = new View({
|
|
|
|
|
center: proj.fromLonLat([3.88, 43.605]),
|
2020-07-23 21:09:05 +00:00
|
|
|
|
zoom: 14,
|
2020-07-23 18:49:01 +00:00
|
|
|
|
maxZoom: 22,
|
|
|
|
|
constrainResolution: true,
|
2020-07-23 15:29:35 +00:00
|
|
|
|
});
|
|
|
|
|
|
2020-07-17 17:17:06 +00:00
|
|
|
|
const map = new Map({
|
|
|
|
|
target,
|
|
|
|
|
layers: [
|
|
|
|
|
backgroundLayer,
|
2020-07-17 17:59:34 +00:00
|
|
|
|
segmentsBorderLayer,
|
|
|
|
|
segmentsInnerLayer,
|
|
|
|
|
stopsLayer,
|
2020-07-17 17:17:06 +00:00
|
|
|
|
],
|
2020-07-23 18:49:01 +00:00
|
|
|
|
view,
|
|
|
|
|
});
|
|
|
|
|
|
2020-07-23 23:32:39 +00:00
|
|
|
|
// Run courses simulation
|
2020-07-23 18:49:01 +00:00
|
|
|
|
const simulInstance = simulation.start();
|
|
|
|
|
|
2020-07-23 23:32:39 +00:00
|
|
|
|
// Course on which the view is currently focused
|
|
|
|
|
let focusedCourse = null;
|
|
|
|
|
const focusZoom = 17;
|
|
|
|
|
|
|
|
|
|
const startFocus = courseId =>
|
|
|
|
|
{
|
|
|
|
|
if (courseId in simulInstance.courses)
|
|
|
|
|
{
|
|
|
|
|
const course = simulInstance.courses[courseId];
|
|
|
|
|
view.animate({
|
|
|
|
|
center: course.position,
|
|
|
|
|
zoom: focusZoom,
|
|
|
|
|
duration: 500,
|
|
|
|
|
}, () => focusedCourse = courseId);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const stopFocus = () =>
|
|
|
|
|
{
|
|
|
|
|
focusedCourse = null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Draw courses directly on the map
|
2020-07-23 18:49:01 +00:00
|
|
|
|
map.on('postcompose', ev =>
|
|
|
|
|
{
|
|
|
|
|
simulInstance.update();
|
|
|
|
|
|
|
|
|
|
// The normal way to access a layer’s vector context is through the
|
|
|
|
|
// `postrender` event of that layer. However, `postrender` is not
|
|
|
|
|
// triggered when no feature of the layer is inside the current
|
|
|
|
|
// bounding box, but we want to draw vehicles in between stops even
|
|
|
|
|
// if no stop is visible. This hack listens to the global `postcompose`
|
|
|
|
|
// event, which is always triggered at every frame, and reconstructs
|
|
|
|
|
// the stops layer’s vector context from internal variables
|
|
|
|
|
if (stopsLayer.renderer_)
|
|
|
|
|
{
|
|
|
|
|
ev.context = stopsLayer.renderer_.context;
|
|
|
|
|
ev.inversePixelTransform
|
|
|
|
|
= stopsLayer.renderer_.inversePixelTransform;
|
|
|
|
|
|
|
|
|
|
const ctx = getVectorContext(ev);
|
2020-07-23 21:09:05 +00:00
|
|
|
|
let rotation = 0;
|
2020-07-23 18:49:01 +00:00
|
|
|
|
|
|
|
|
|
for (let course of Object.values(simulInstance.courses))
|
|
|
|
|
{
|
2020-07-23 21:09:05 +00:00
|
|
|
|
const color = network.lines[course.line].color;
|
|
|
|
|
const style = getCourseStyle(color);
|
|
|
|
|
|
2020-07-23 22:18:30 +00:00
|
|
|
|
style.getImage().setRotation(course.angle);
|
2020-07-23 21:09:05 +00:00
|
|
|
|
ctx.setStyle(style);
|
|
|
|
|
|
2020-07-23 18:49:01 +00:00
|
|
|
|
const point = new Point(course.position);
|
|
|
|
|
ctx.drawGeometry(point);
|
2020-07-23 23:32:39 +00:00
|
|
|
|
|
|
|
|
|
if (course.id === focusedCourse)
|
|
|
|
|
{
|
|
|
|
|
view.setCenter(course.position);
|
|
|
|
|
// view.setZoom(focus);
|
|
|
|
|
}
|
2020-07-23 18:49:01 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
map.render();
|
2020-07-17 17:17:06 +00:00
|
|
|
|
});
|
|
|
|
|
|
2020-07-23 18:49:01 +00:00
|
|
|
|
map.render();
|
2020-07-23 23:32:39 +00:00
|
|
|
|
|
|
|
|
|
map.on('singleclick', ev =>
|
|
|
|
|
{
|
|
|
|
|
const mousePixel = map.getPixelFromCoordinate(ev.coordinate);
|
|
|
|
|
const maxDistance = sizes.courseSize + sizes.courseOuterBorder;
|
|
|
|
|
|
|
|
|
|
for (let course of Object.values(simulInstance.courses))
|
|
|
|
|
{
|
|
|
|
|
const coursePixel = map.getPixelFromCoordinate(course.position);
|
|
|
|
|
const dx = mousePixel[0] - coursePixel[0];
|
|
|
|
|
const dy = mousePixel[1] - coursePixel[1];
|
|
|
|
|
const distance = dx * dx + dy * dy;
|
|
|
|
|
|
|
|
|
|
if (distance <= maxDistance * maxDistance)
|
|
|
|
|
{
|
|
|
|
|
startFocus(course.id);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Clicking anywhere else resets focus
|
|
|
|
|
stopFocus();
|
|
|
|
|
});
|
|
|
|
|
|
2020-07-17 17:17:06 +00:00
|
|
|
|
return map;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
exports.createMap = createMap;
|