Implement clicking on course & follow mode

This commit is contained in:
Mattéo Delabre 2022-07-09 18:14:00 -04:00
parent 3aa2043c30
commit 9e41bf8079
Signed by: matteo
GPG Key ID: AE3FBD02DC583ABB
3 changed files with 139 additions and 189 deletions

View File

@ -1,13 +1,9 @@
import network from "../data/network.json"; import network from "../data/network.json";
import * as simulation from "../data/simulation.js"; import * as simulation from "../data/simulation.js";
import * as map from "./map/index.js"; import Map from "./map/index.js";
// Run courses simulation // Run courses simulation
const coursesSimulation = simulation.start(); const simstate = simulation.start();
let selectedCourse = null;
window.__courses = coursesSimulation.courses;
window.__network = network;
// Create display panel // Create display panel
const panel = document.querySelector("#panel"); const panel = document.querySelector("#panel");
@ -29,9 +25,9 @@ const timeToHTML = time => {
} }
}; };
setInterval(() => { const updatePanel = () => {
const vehicleCount = Object.values(coursesSimulation.courses).length; const vehicleCount = Object.values(simstate.courses).length;
const movingVehicles = Object.values(coursesSimulation.courses).filter( const movingVehicles = Object.values(simstate.courses).filter(
course => course.properties.speed > 0 course => course.properties.speed > 0
).length; ).length;
@ -49,8 +45,8 @@ setInterval(() => {
</dl> </dl>
`; `;
if (selectedCourse !== null && selectedCourse in coursesSimulation.courses) { if (map.selectedCourse !== null && map.selectedCourse in simstate.courses) {
const course = coursesSimulation.courses[selectedCourse]; const course = simstate.courses[map.selectedCourse].properties;
const stopToHTML = stopId => stopId in network.stops ? const stopToHTML = stopId => stopId in network.stops ?
network.stops[stopId].properties.name : network.stops[stopId].properties.name :
@ -75,9 +71,10 @@ setInterval(() => {
} }
html += ` html += `
<h2>Véhicule</h2>
<dl> <dl>
<dt>ID</dt> <dt>ID</dt>
<dd>${selectedCourse}</dd> <dd>${map.selectedCourse}</dd>
<dt>Ligne</dt> <dt>Ligne</dt>
<dd>${course.line}</dd> <dd>${course.line}</dd>
@ -115,17 +112,30 @@ setInterval(() => {
} }
panel.innerHTML = html; panel.innerHTML = html;
}, 1000); };
// Create the network and courses map setInterval(updatePanel, 1000);
window.__map = map.create(/* map = */ "map", coursesSimulation, courses => {
const index = courses.indexOf(selectedCourse);
if (courses.length === 0) { const map = new Map("map", simstate, {
selectedCourse = null; onVehicleClick(courses) {
} else if (index === -1) { courses = [...courses];
selectedCourse = courses[0]; courses.sort();
} else {
selectedCourse = courses[(index + 1) % courses.length]; 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;

View File

@ -1,5 +1,5 @@
import "maplibre-gl/dist/maplibre-gl.css"; 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 network from "./network";
import * as vehicles from "./vehicles"; import * as vehicles from "./vehicles";
@ -16,33 +16,81 @@ const bounds = [
"4.092750549316406", "43.73736766145917", "4.092750549316406", "43.73736766145917",
]; ];
export const create = (target, simulation, onClick) => { export default class Map {
const map = new Map({ /**
container: target, * Instantiate a map.
style, * @param {string|HTMLElement} target The container to add the map to.
bounds, * @param {Object} simstate Course simulation state.
* @param {Object.<callable>} handlers Handlers for the map events.
*/
constructor(target, simstate, handlers) {
this.renderer = new maplibre.Map({
container: target,
style,
bounds,
maplibreLogo: true, maplibreLogo: true,
maxPitch: 70, maxPitch: 70,
}); });
map.addControl(new NavigationControl()); this.renderer.addControl(new maplibre.NavigationControl());
const animate = () => { this.renderer.on("load", () => {
simulation.update(); network.addLayers(this.renderer);
vehicles.update(map, simulation.courses); vehicles.addLayers(this.renderer, handlers);
requestAnimationFrame(animate);
};
map.on("load", () => { // Move 3D buildings to the front of custom layers
network.addLayers(map); this.renderer.moveLayer("building-3d");
vehicles.addLayers(map); this.start();
});
// Move 3D buildings to the front of custom layers // ID of the currently selected course
map.moveLayer("building-3d"); 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));
}
}

View File

@ -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 * as turfHelpers from "@turf/helpers";
import { scaleZoom, makeBorderColor, makeCourseColor } from "./common"; import { scaleZoom, makeBorderColor, makeCourseColor } from "./common";
import network from "../../data/network.json"; import network from "../../data/network.json";
const VEHICLE_SIZE = 15;
const VEHICLE_BORDER = 10;
const makeVehicleIcon = size => { const makeVehicleIcon = size => {
const canvas = window.document.createElement("canvas"); const canvas = window.document.createElement("canvas");
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
@ -20,8 +13,8 @@ const makeVehicleIcon = size => {
const cx = size / 2; const cx = size / 2;
const cy = size / 2; const cy = size / 2;
const iconSize = 0.4 * size; const iconSize = 0.7 * size;
const borderSize = 0.2 * size; const borderSize = 0.3 * size;
context.fillStyle = "black"; context.fillStyle = "black";
context.strokeStyle = "black"; context.strokeStyle = "black";
@ -47,8 +40,9 @@ const makeVehicleIcon = size => {
/** /**
* Add layers that display the live vehicle positions on the map. * Add layers that display the live vehicle positions on the map.
* @param {Map} map The map to add the layers to. * @param {Map} map The map to add the layers to.
* @param {Object.<callable>} handlers Handlers for the map events.
*/ */
export const addLayers = map => { export const addLayers = (map, handlers) => {
map.addImage("vehicle", makeVehicleIcon(128), {sdf: true}); map.addImage("vehicle", makeVehicleIcon(128), {sdf: true});
map.addSource("vehicles", { 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)) 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 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
// /* 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);
// });
// };