Implement clicking on course & follow mode
This commit is contained in:
parent
3aa2043c30
commit
9e41bf8079
|
@ -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(() => {
|
|||
</dl>
|
||||
`;
|
||||
|
||||
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 += `
|
||||
<h2>Véhicule</h2>
|
||||
<dl>
|
||||
<dt>ID</dt>
|
||||
<dd>${selectedCourse}</dd>
|
||||
<dd>${map.selectedCourse}</dd>
|
||||
|
||||
<dt>Ligne</dt>
|
||||
<dd>${course.line}</dd>
|
||||
|
@ -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;
|
||||
|
|
|
@ -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.<callable>} 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.<callable>} 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);
|
||||
// });
|
||||
// };
|
||||
|
|
Loading…
Reference in New Issue