require("ol/ol.css"); const { Map, View } = require("ol"); const GeoJSON = require("ol/format/GeoJSON").default; const reader = new GeoJSON({ featureProjection: "EPSG:3857" }); 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 Point = require("ol/geom/Point").default; const proj = require("ol/proj"); const { Style, Fill, Stroke, Circle, Icon } = require("ol/style"); const colorModule = require("color"); const mapboxToken = "pk.eyJ1IjoibWF0dGVvZGVsYWJyZSIsImEiOiJja2NxaTUyMmUwcmFhMn\ h0NmFsdzQ3emxqIn0.cyxF0h36emIMTk3cc4VqUw"; const simulation = require("../tam/simulation"); const network = require("../tam/network.json"); const lineFeaturesOrder = (feature1, feature2) => { const lines1 = feature1.get("lines"); if (lines1.length === 0) { return -1; } const lines2 = feature2.get("lines"); if (lines2.length === 0) { return 1; } return Math.min(...lines1) - Math.min(...lines2); }; const makeDataSources = () => { const segmentsSource = new VectorSource(); const stopsSource = new VectorSource(); const readFeatures = hash => Object.values(hash).map(json => { json.properties.lines = json.properties.routes.filter( // Only consider normal routes (excluding alternate routes) ([lineRef, routeRef]) => network.lines[lineRef].routes[routeRef].state === "normal" ).map(([lineRef]) => lineRef); if (json.properties.lines.length >= 1) { json.properties.colors = json.properties.lines.map( lineRef => network.lines[lineRef].color ); } else { json.properties.colors = ["#FFFFFF"]; } return reader.readFeature(json); }); segmentsSource.addFeatures(readFeatures(network.segments)); stopsSource.addFeatures(readFeatures(network.stops)); return { segmentsSource, stopsSource }; }; const makeBorderColor = mainColor => { const hsl = colorModule(mainColor).hsl(); hsl.color = Math.max(0, hsl.color[2] -= 20); return hsl.hex(); }; const makeCourseColor = mainColor => { const hsl = colorModule(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 = lineColor => { if (!(lineColor in courseStyles)) { 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(); } courseStyles[lineColor] = new Style({ image: new Icon({ img: icon, imgSize: [icon.width, icon.height] }) }); } return courseStyles[lineColor]; }; 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, renderOrder: lineFeaturesOrder, style: segmentBorderStyle, updateWhileInteracting: true, updateWhileAnimating: true }); const segmentsInnerLayer = new VectorLayer({ source: segmentsSource, renderOrder: lineFeaturesOrder, style: segmentInnerStyle, updateWhileInteracting: true, updateWhileAnimating: true }); const stopsLayer = new VectorLayer({ source: stopsSource, renderOrder: lineFeaturesOrder, 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 startFocus = courseId => { if (courseId in simulInstance.courses) { const course = simulInstance.courses[courseId]; view.animate({ center: course.position, 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 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); for (const 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); } } } map.render(); }); map.render(); map.on("singleclick", ev => { const mousePixel = map.getPixelFromCoordinate(ev.coordinate); const maxDistance = sizes.courseSize + sizes.courseOuterBorder; for (const 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;