tracktracker/src/front/map.js

334 lines
8.9 KiB
JavaScript
Raw Normal View History

2020-07-17 17:17:06 +00:00
require('ol/ol.css');
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');
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');
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
{
const segmentsSource = new VectorSource();
const stopsSource = new VectorSource();
2020-07-17 17:17:06 +00:00
segmentsSource.addFeatures(
2020-07-22 21:11:00 +00:00
Object.values(network.segments).map(({routes, points}) =>
new Feature({
2020-07-22 21:11:00 +00:00
colors: getRouteColors(routes),
geometry: new LineString(points.map(
({lat, lon}) => proj.fromLonLat([lon, lat])
)),
})
)
);
2020-07-17 17:17:06 +00:00
stopsSource.addFeatures(
2020-07-22 21:11:00 +00:00
Object.values(network.stops).map(({routes, lon, lat}) =>
new Feature({
2020-07-22 21:11:00 +00:00
colors: getRouteColors(routes),
geometry: new Point(proj.fromLonLat([lon, lat])),
})
)
);
2020-07-17 17:17:06 +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({
stroke: new Stroke({
color: makeBorderColor(feature.get('colors')[0]),
2020-07-23 23:32:39 +00:00
width: sizes.segmentOuter,
}),
});
2020-07-23 18:49:01 +00:00
const segmentInnerStyle = feature => new Style({
stroke: new Stroke({
color: feature.get('colors')[0],
2020-07-23 23:32:39 +00:00
width: sizes.segmentInner,
}),
});
2020-07-23 18:49:01 +00:00
const stopStyle = feature => new Style({
image: new Circle({
fill: new Fill({
color: feature.get('colors')[0],
}),
stroke: new Stroke({
color: makeBorderColor(feature.get('colors')[0]),
2020-07-23 23:32:39 +00:00
width: sizes.stopBorder,
}),
2020-07-23 23:32:39 +00:00
radius: sizes.stopRadius,
}),
});
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();
const segmentsBorderLayer = new VectorLayer({
source: segmentsSource,
2020-07-23 18:49:01 +00:00
style: segmentBorderStyle,
updateWhileInteracting: true,
updateWhileAnimating: true,
});
const segmentsInnerLayer = new VectorLayer({
source: segmentsSource,
2020-07-23 18:49:01 +00:00
style: segmentInnerStyle,
updateWhileInteracting: true,
updateWhileAnimating: true,
});
2020-07-17 17:17:06 +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
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,
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 layers 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 layers 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;