tracktracker/src/front/map.js

334 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

require('ol/ol.css');
const axios = require('axios');
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;
const {getVectorContext} = require('ol/render');
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');
const {Style, Fill, Stroke, Circle, Icon} = require('ol/style');
const color = require('color');
const mapboxToken = `pk.eyJ1IjoibWF0dGVvZGVsYWJyZSIsImEiOiJja2NxaTUyMmUwcmFhMn\
h0NmFsdzQ3emxqIn0.cyxF0h36emIMTk3cc4VqUw`;
const simulation = require('../tam/simulation');
const network = require('../tam/network.json');
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'];
};
const makeDataSources = () =>
{
const segmentsSource = new VectorSource();
const stopsSource = new VectorSource();
segmentsSource.addFeatures(
Object.values(network.segments).map(({routes, points}) =>
new Feature({
colors: getRouteColors(routes),
geometry: new LineString(points.map(
({lat, lon}) => proj.fromLonLat([lon, lat])
)),
})
)
);
stopsSource.addFeatures(
Object.values(network.stops).map(({routes, lon, lat}) =>
new Feature({
colors: getRouteColors(routes),
geometry: new Point(proj.fromLonLat([lon, lat])),
})
)
);
return {segmentsSource, stopsSource};
};
const makeBorderColor = mainColor =>
{
const hsl = color(mainColor).hsl();
hsl.color = Math.max(0, hsl.color[2] -= 20);
return hsl.hex();
};
const makeCourseColor = mainColor =>
{
const hsl = color(mainColor).hsl();
hsl.color = Math.max(0, hsl.color[2] += 10);
return hsl.hex();
};
const sizes = {
segmentOuter: 8,
segmentInner: 6,
stopRadius: 6,
stopBorder: 1.5,
courseSize: 15,
courseOuterBorder: 13,
courseBorder: 10,
courseInnerBorder: 7,
};
const segmentBorderStyle = feature => new Style({
stroke: new Stroke({
color: makeBorderColor(feature.get('colors')[0]),
width: sizes.segmentOuter,
}),
});
const segmentInnerStyle = feature => new Style({
stroke: new Stroke({
color: feature.get('colors')[0],
width: sizes.segmentInner,
}),
});
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]),
width: sizes.stopBorder,
}),
radius: sizes.stopRadius,
}),
});
const courseStyles = {};
const getCourseStyle = color =>
{
if (!(color in courseStyles))
{
const icon = document.createElement('canvas');
const shapeSize = sizes.courseSize;
const iconSize = sizes.courseSize + sizes.courseOuterBorder;
icon.width = iconSize;
icon.height = iconSize;
const cx = icon.width / 2;
const cy = icon.height / 2;
const iconCtx = icon.getContext('2d');
for (let [color, size] of [
[makeBorderColor(color), sizes.courseOuterBorder],
[color, sizes.courseBorder],
[makeCourseColor(color), sizes.courseInnerBorder]
])
{
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];
};
const createMap = target =>
{
// 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,
});
// Static data overlay
const {segmentsSource, stopsSource} = makeDataSources();
const segmentsBorderLayer = new VectorLayer({
source: segmentsSource,
style: segmentBorderStyle,
updateWhileInteracting: true,
updateWhileAnimating: true,
});
const segmentsInnerLayer = new VectorLayer({
source: segmentsSource,
style: segmentInnerStyle,
updateWhileInteracting: true,
updateWhileAnimating: true,
});
const stopsLayer = new VectorLayer({
source: stopsSource,
style: stopStyle,
minZoom: 13,
updateWhileInteracting: true,
updateWhileAnimating: true,
});
// Setup map
const view = new View({
center: proj.fromLonLat([3.88, 43.605]),
zoom: 14,
maxZoom: 22,
constrainResolution: true,
});
const map = new Map({
target,
layers: [
backgroundLayer,
segmentsBorderLayer,
segmentsInnerLayer,
stopsLayer,
],
view,
});
// Run courses simulation
const simulInstance = simulation.start();
// 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
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);
let rotation = 0;
for (let course of Object.values(simulInstance.courses))
{
const color = network.lines[course.line].color;
const style = getCourseStyle(color);
style.getImage().setRotation(course.angle);
ctx.setStyle(style);
const point = new Point(course.position);
ctx.drawGeometry(point);
if (course.id === focusedCourse)
{
view.setCenter(course.position);
// view.setZoom(focus);
}
}
}
map.render();
});
map.render();
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();
});
return map;
};
exports.createMap = createMap;