diff --git a/src/front/index.js b/src/front/index.js
index 1a555cc..2c5f17d 100644
--- a/src/front/index.js
+++ b/src/front/index.js
@@ -1,13 +1,9 @@
import network from "../data/network.json";
import * as simulation from "../data/simulation.js";
-import * as map from "./map/index.js";
+import Map from "./map/index.js";
// Run courses simulation
-const coursesSimulation = simulation.start();
-let selectedCourse = null;
-
-window.__courses = coursesSimulation.courses;
-window.__network = network;
+const simstate = simulation.start();
// Create display panel
const panel = document.querySelector("#panel");
@@ -29,9 +25,9 @@ const timeToHTML = time => {
}
};
-setInterval(() => {
- const vehicleCount = Object.values(coursesSimulation.courses).length;
- const movingVehicles = Object.values(coursesSimulation.courses).filter(
+const updatePanel = () => {
+ const vehicleCount = Object.values(simstate.courses).length;
+ const movingVehicles = Object.values(simstate.courses).filter(
course => course.properties.speed > 0
).length;
@@ -49,8 +45,8 @@ setInterval(() => {
`;
- if (selectedCourse !== null && selectedCourse in coursesSimulation.courses) {
- const course = coursesSimulation.courses[selectedCourse];
+ if (map.selectedCourse !== null && map.selectedCourse in simstate.courses) {
+ const course = simstate.courses[map.selectedCourse].properties;
const stopToHTML = stopId => stopId in network.stops ?
network.stops[stopId].properties.name :
@@ -75,9 +71,10 @@ setInterval(() => {
}
html += `
+
Véhicule
- ID
- - ${selectedCourse}
+ - ${map.selectedCourse}
- Ligne
- ${course.line}
@@ -115,17 +112,30 @@ setInterval(() => {
}
panel.innerHTML = html;
-}, 1000);
+};
-// Create the network and courses map
-window.__map = map.create(/* map = */ "map", coursesSimulation, courses => {
- const index = courses.indexOf(selectedCourse);
+setInterval(updatePanel, 1000);
- if (courses.length === 0) {
- selectedCourse = null;
- } else if (index === -1) {
- selectedCourse = courses[0];
- } else {
- selectedCourse = courses[(index + 1) % courses.length];
+const map = new Map("map", simstate, {
+ onVehicleClick(courses) {
+ courses = [...courses];
+ courses.sort();
+
+ const index = courses.indexOf(map.selectedCourse);
+
+ if (courses.length === 0) {
+ map.select(null);
+ } else if (index === -1) {
+ map.select(courses[0]);
+ } else {
+ map.select(courses[(index + 1) % courses.length]);
+ }
+
+ updatePanel();
}
});
+
+// Reference as globals to facilitate debugging
+window.__map = map;
+window.__simstate = simstate.courses;
+window.__network = network;
diff --git a/src/front/map/index.js b/src/front/map/index.js
index d67413e..e152e70 100644
--- a/src/front/map/index.js
+++ b/src/front/map/index.js
@@ -1,5 +1,5 @@
import "maplibre-gl/dist/maplibre-gl.css";
-import { Map, NavigationControl } from "maplibre-gl";
+import * as maplibre from "maplibre-gl";
import * as network from "./network";
import * as vehicles from "./vehicles";
@@ -16,33 +16,81 @@ const bounds = [
"4.092750549316406", "43.73736766145917",
];
-export const create = (target, simulation, onClick) => {
- const map = new Map({
- container: target,
- style,
- bounds,
+export default class Map {
+ /**
+ * Instantiate a map.
+ * @param {string|HTMLElement} target The container to add the map to.
+ * @param {Object} simstate Course simulation state.
+ * @param {Object.} handlers Handlers for the map events.
+ */
+ constructor(target, simstate, handlers) {
+ this.renderer = new maplibre.Map({
+ container: target,
+ style,
+ bounds,
- maplibreLogo: true,
- maxPitch: 70,
- });
+ maplibreLogo: true,
+ maxPitch: 70,
+ });
- map.addControl(new NavigationControl());
+ this.renderer.addControl(new maplibre.NavigationControl());
- const animate = () => {
- simulation.update();
- vehicles.update(map, simulation.courses);
- requestAnimationFrame(animate);
- };
+ this.renderer.on("load", () => {
+ network.addLayers(this.renderer);
+ vehicles.addLayers(this.renderer, handlers);
- map.on("load", () => {
- network.addLayers(map);
- vehicles.addLayers(map);
+ // Move 3D buildings to the front of custom layers
+ this.renderer.moveLayer("building-3d");
+ this.start();
+ });
- // Move 3D buildings to the front of custom layers
- map.moveLayer("building-3d");
+ // ID of the currently selected course
+ this.selectedCourse = null;
- animate();
- });
+ this.animation = null;
+ this.simstate = simstate;
+ this.handlers = handlers;
+ }
- return map;
-};
+ /** Start animating vehicles on the map. */
+ start() {
+ this._animate();
+ }
+
+ /** Stop the vehicle animation. */
+ stop() {
+ cancelAnimationFrame(this.animation);
+ this.animation = null;
+ }
+
+ /**
+ * Select or unselect a vehicle.
+ * @param {string?} id The ID of the vehicle to select, or null to unselect.
+ */
+ select(courseId) {
+ this.selectedCourse = courseId;
+
+ const course = this.simstate.courses[courseId];
+ this.renderer.flyTo({
+ center: course.geometry.coordinates,
+ bearing: course.properties.bearing,
+ pitch: 60,
+ zoom: 20,
+ });
+ }
+
+ _animate() {
+ this.simstate.update();
+ vehicles.update(this.renderer, this.simstate.courses);
+
+ if (this.selectedCourse !== null && !this.renderer.isMoving()) {
+ const course = this.simstate.courses[this.selectedCourse];
+ this.renderer.jumpTo({
+ center: course.geometry.coordinates,
+ bearing: course.properties.bearing,
+ });
+ }
+
+ this.animation = requestAnimationFrame(this._animate.bind(this));
+ }
+}
diff --git a/src/front/map/vehicles.js b/src/front/map/vehicles.js
index 12bc260..bc65aeb 100644
--- a/src/front/map/vehicles.js
+++ b/src/front/map/vehicles.js
@@ -1,15 +1,8 @@
-// import { getVectorContext } from "ol/render";
-// import Point from "ol/geom/Point";
-// import { fromExtent } from 'ol/geom/Polygon';
-// import { Style, Icon } from "ol/style";
import * as turfHelpers from "@turf/helpers";
import { scaleZoom, makeBorderColor, makeCourseColor } from "./common";
import network from "../../data/network.json";
-const VEHICLE_SIZE = 15;
-const VEHICLE_BORDER = 10;
-
const makeVehicleIcon = size => {
const canvas = window.document.createElement("canvas");
const context = canvas.getContext("2d");
@@ -20,8 +13,8 @@ const makeVehicleIcon = size => {
const cx = size / 2;
const cy = size / 2;
- const iconSize = 0.4 * size;
- const borderSize = 0.2 * size;
+ const iconSize = 0.7 * size;
+ const borderSize = 0.3 * size;
context.fillStyle = "black";
context.strokeStyle = "black";
@@ -47,8 +40,9 @@ const makeVehicleIcon = size => {
/**
* Add layers that display the live vehicle positions on the map.
* @param {Map} map The map to add the layers to.
+ * @param {Object.} handlers Handlers for the map events.
*/
-export const addLayers = map => {
+export const addLayers = (map, handlers) => {
map.addImage("vehicle", makeVehicleIcon(128), {sdf: true});
map.addSource("vehicles", {
@@ -77,6 +71,37 @@ export const addLayers = map => {
},
});
}
+
+ map.on("click", "vehicles-outer", event => {
+ // Sort clicked courses in increasing order of departing/arrival time
+ event.features.sort((feature1, feature2) => {
+ const course1 = feature1.properties;
+ const course2 = feature2.properties;
+
+ const time1 = (
+ course1.speed > 0 ? course1.arrivalTime : course1.departureTime
+ );
+
+ const time2 = (
+ course2.speed > 0 ? course2.arrivalTime : course2.departureTime
+ );
+
+ return time1 - time2;
+ });
+
+ handlers.onVehicleClick(event.features.map(
+ feature => feature.properties.id
+ ));
+ });
+
+ // Show a pointer cursor when hovering over a vehicle
+ map.on("mouseenter", "vehicles-outer", () => {
+ map.getCanvas().style.cursor = "pointer";
+ });
+
+ map.on("mouseleave", "vehicles-outer", () => {
+ map.getCanvas().style.cursor = '';
+ });
};
/**
@@ -98,136 +123,3 @@ export const update = (map, courses) => {
turfHelpers.featureCollection(Object.values(courses))
);
};
-
-// const courseStyle = cacheStyle(
-// lineColor => {
-// const icon = window.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 (const [color, size] of [
-// [makeBorderColor(lineColor), sizes.courseOuterBorder],
-// [lineColor, sizes.courseBorder],
-// [makeCourseColor(lineColor), 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();
-// }
-
-// return new Style({
-// image: new Icon({
-// img: icon,
-// imgSize: [icon.width, icon.height]
-// })
-// });
-// },
-// );
-
-// export const setupCoursesAnimation = (
-// map, coursesSimulation, stopsLayer, onClick
-// ) => {
-// const view = map.getView();
-
-// // Draw courses directly on the map
-// map.on("postcompose", ev => {
-// coursesSimulation.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
-// /* eslint-disable no-underscore-dangle */
-// if (stopsLayer.renderer_) {
-// ev.context = stopsLayer.renderer_.context;
-// ev.inversePixelTransform =
-// stopsLayer.renderer_.inversePixelTransform;
-// /* eslint-enable no-underscore-dangle */
-
-// const ctx = getVectorContext(ev);
-// const bbox = fromExtent(map.getView().calculateExtent());
-// bbox.scale(1.05);
-
-// for (const course of Object.values(coursesSimulation.courses)) {
-// if (!bbox.intersectsCoordinate(course.position)) {
-// continue;
-// }
-
-// const point = new Point(course.position);
-// const color = network.lines[course.line].color;
-// const style = courseStyle(color);
-
-// style.getImage().setRotation(
-// view.getRotation() +
-// course.angle
-// );
-
-// ctx.setStyle(style);
-// ctx.drawGeometry(point);
-// }
-// }
-
-// map.render();
-// });
-
-// map.on("singleclick", ev => {
-// const mousePixel = map.getPixelFromCoordinate(ev.coordinate);
-// const maxDistance = sizes.courseSize + sizes.courseInnerBorder;
-// const clicked = [];
-
-// for (const course of Object.values(coursesSimulation.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) {
-// clicked.push(course.id);
-// }
-// }
-
-// // Sort selected courses in increasing departing/arrival time
-// clicked.sort((id1, id2) => {
-// const course1 = coursesSimulation.courses[id1];
-// const course2 = coursesSimulation.courses[id2];
-
-// const time1 = (
-// course1.state === "moving"
-// ? course1.arrivalTime
-// : course1.departureTime
-// );
-
-// const time2 = (
-// course2.state === "moving"
-// ? course2.arrivalTime
-// : course2.departureTime
-// );
-
-// return time1 - time2;
-// });
-
-// onClick(clicked);
-// });
-// };