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 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);
|
const map = new Map("map", simstate, {
|
||||||
|
onVehicleClick(courses) {
|
||||||
|
courses = [...courses];
|
||||||
|
courses.sort();
|
||||||
|
|
||||||
|
const index = courses.indexOf(map.selectedCourse);
|
||||||
|
|
||||||
if (courses.length === 0) {
|
if (courses.length === 0) {
|
||||||
selectedCourse = null;
|
map.select(null);
|
||||||
} else if (index === -1) {
|
} else if (index === -1) {
|
||||||
selectedCourse = courses[0];
|
map.select(courses[0]);
|
||||||
} else {
|
} else {
|
||||||
selectedCourse = courses[(index + 1) % courses.length];
|
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 "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,8 +16,15 @@ const bounds = [
|
||||||
"4.092750549316406", "43.73736766145917",
|
"4.092750549316406", "43.73736766145917",
|
||||||
];
|
];
|
||||||
|
|
||||||
export const create = (target, simulation, onClick) => {
|
export default class Map {
|
||||||
const map = new 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,
|
container: target,
|
||||||
style,
|
style,
|
||||||
bounds,
|
bounds,
|
||||||
|
@ -26,23 +33,64 @@ export const create = (target, simulation, onClick) => {
|
||||||
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", () => {
|
|
||||||
network.addLayers(map);
|
|
||||||
vehicles.addLayers(map);
|
|
||||||
|
|
||||||
// Move 3D buildings to the front of custom layers
|
// Move 3D buildings to the front of custom layers
|
||||||
map.moveLayer("building-3d");
|
this.renderer.moveLayer("building-3d");
|
||||||
|
this.start();
|
||||||
animate();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return map;
|
// ID of the currently selected course
|
||||||
};
|
this.selectedCourse = null;
|
||||||
|
|
||||||
|
this.animation = null;
|
||||||
|
this.simstate = simstate;
|
||||||
|
this.handlers = handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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 * 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 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