import * as turfHelpers from "@turf/helpers"; import turfClone from "@turf/clone"; import network from "../../data/network.json"; import * as routing from "../../data/routing.js"; import { makeBorderColor, scaleZoom } from "./common"; const MIN_ZOOM_SEGMENTS = 9; const MIN_ZOOM_STOPS = 11; const MAX_ZOOM = 22; const BORDER_SIZE = 3; const SEGMENT_SIZE = 6; const STOP_SIZE = 12; /** Compute a line offset to share a line span with other adjacent lines. */ const getOffset = (size, index, count) => ( -size / 2 + size / count / 2 + index * size / count ); /** Get the collection of line segments. */ const getSegments = () => { // Aggregate segments spanning multiple routes const groupedSegments = {}; for (const [lineRef, line] of Object.entries(network.lines)) { for (const route of line.routes) { for (let i = 0; i + 1 < route.stops.length; ++i) { const stop1 = network.stops[route.stops[i]].id; const stop2 = network.stops[route.stops[i + 1]].id; const waypoints = routing.findPath(stop1, stop2); for (let j = 0; j + 1 < waypoints.length; ++j) { const point1 = waypoints[j]; const point2 = waypoints[j + 1]; const id = `${point1}-${point2}`; if (id in groupedSegments) { groupedSegments[id].properties.lines.add(lineRef); } else { groupedSegments[id] = ( turfClone(network.navigation[point1][point2]) ); groupedSegments[id].properties.lines = new Set(); groupedSegments[id].properties.lines.add(lineRef); } } } } } const segments = []; for (const segment of Object.values(groupedSegments)) { const lines = [...segment.properties.lines]; lines.sort(); const count = lines.length; // Duplicate and offset segments for each route for (const [index, line] of lines.entries()) { const feature = turfClone(segment); const props = feature.properties; delete props.lines; props.line = line; props.innerSize = SEGMENT_SIZE / count; props.innerColor = network.lines[line].color; props.innerOffset = getOffset(SEGMENT_SIZE, index, count); props.outerSize = (SEGMENT_SIZE + BORDER_SIZE) / count; props.outerColor = makeBorderColor(props.innerColor); props.outerOffset = getOffset(SEGMENT_SIZE + BORDER_SIZE, index, count); segments.push(feature); } } return turfHelpers.featureCollection(segments); }; /** Get the collection of stops. */ const getStops = () => { const stops = []; for (const stop of Object.values(network.stops)) { const lines = [...new Set( stop.properties.routes.map(([lineRef]) => lineRef) )]; lines.sort(); const count = lines.length; // Duplicate and offset stops for each route for (const [index, line] of lines.entries()) { const feature = turfClone(stop); const props = feature.properties; props.line = line; props.innerSize = (count - index) * STOP_SIZE / 2 / count; props.innerColor = network.lines[line].color; if (index === 0) { props.outerSize = (STOP_SIZE + BORDER_SIZE) / 2; } else { props.outerSize = 0; } props.outerColor = makeBorderColor(props.innerColor); stops.push(feature); } } return turfHelpers.featureCollection(stops); }; /** * Add layers that display the transit network on the map. * @param {Map} map The map to add the layers to. */ export const addLayers = map => { map.addSource("lines", { type: "geojson", data: getSegments(), }); map.addSource("stops", { type: "geojson", data: getStops(), }); for (const lineRef of Object.keys(network.lines)) { for (const kind of ["outer", "inner"]) { map.addLayer({ id: `line-${lineRef}-${kind}`, type: "line", source: "lines", filter: ["==", "line", lineRef], minzoom: MIN_ZOOM_SEGMENTS, layout: { "line-cap": "round", "line-join": "round", }, paint: { "line-color": ["get", `${kind}Color`], "line-width": scaleZoom(["get", `${kind}Size`]), "line-offset": scaleZoom(["get", `${kind}Offset`]), }, }); } } for (const lineRef of Object.keys(network.lines)) { for (const kind of ["outer", "inner"]) { map.addLayer({ id: `stops-${lineRef}-${kind}`, type: "circle", source: "stops", filter: ["==", "line", lineRef], minzoom: MIN_ZOOM_STOPS, paint: { "circle-pitch-alignment": "map", "circle-color": ["get", `${kind}Color`], "circle-radius": scaleZoom(["get", `${kind}Size`]), }, }); } } };