mirror of
https://github.com/HSLdevcom/digitransit-ui
synced 2025-07-27 23:35:15 +02:00
395 lines
13 KiB
JavaScript
395 lines
13 KiB
JavaScript
import { VectorTile } from '@mapbox/vector-tile';
|
|
import Protobuf from 'pbf';
|
|
import pick from 'lodash/pick';
|
|
import { graphql, fetchQuery } from 'react-relay';
|
|
import { DateTime } from 'luxon';
|
|
import {
|
|
drawTerminalIcon,
|
|
drawStopIcon,
|
|
drawHybridStopIcon,
|
|
drawHybridStationIcon,
|
|
} from '../../../util/mapIconUtils';
|
|
import { ExtendedRouteTypes } from '../../../constants';
|
|
import {
|
|
isFeatureLayerEnabled,
|
|
getLayerBaseUrl,
|
|
} from '../../../util/mapLayerUtils';
|
|
import { PREFIX_ITINERARY_SUMMARY, PREFIX_ROUTES } from '../../../util/path';
|
|
import { fetchWithLanguageAndSubscription } from '../../../util/fetchUtils';
|
|
|
|
const stopAlertsQuery = graphql`
|
|
query StopsQuery($stopId: String!, $date: String!) {
|
|
stop: stop(id: $stopId) {
|
|
gtfsId
|
|
alerts: alerts(types: [STOP]) {
|
|
alertEffect
|
|
}
|
|
stoptimes: stoptimesForServiceDate(date: $date, omitCanceled: false) {
|
|
stoptimes {
|
|
serviceDay
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`;
|
|
|
|
function isNull(val) {
|
|
return val === 'null' || val === undefined || val === null;
|
|
}
|
|
|
|
const shouldRenderTerminalIcon = (mode, path, vehicles) => {
|
|
const modesWithoutIcon = ['SUBWAY'];
|
|
const viewsWithoutIcon = [PREFIX_ITINERARY_SUMMARY];
|
|
const selectedMode = vehicles ? Object.values(vehicles)[0]?.mode : undefined;
|
|
if (
|
|
modesWithoutIcon.includes(mode) &&
|
|
(viewsWithoutIcon.some(view => path.includes(view)) ||
|
|
(!!selectedMode &&
|
|
modesWithoutIcon.includes(selectedMode.toUpperCase()) &&
|
|
path.includes(PREFIX_ROUTES)))
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
class Stops {
|
|
constructor(tile, config, mapLayers, relayEnvironment, mergeStops) {
|
|
this.tile = tile;
|
|
this.config = config;
|
|
this.mapLayers = mapLayers;
|
|
this.relayEnvironment = relayEnvironment;
|
|
this.mergeStops = mergeStops;
|
|
}
|
|
|
|
static getName = () => 'stop';
|
|
|
|
drawStop(feature, isHybrid, zoom, minZoom) {
|
|
const isHighlighted =
|
|
this.tile.highlightedStops &&
|
|
this.tile.highlightedStops.includes(feature.properties.gtfsId);
|
|
let hasTrunkRoute = false;
|
|
let hasLocalTramRoute = false;
|
|
const routes = JSON.parse(feature.properties.routes);
|
|
if (
|
|
feature.properties.type === 'BUS' &&
|
|
this.config.useExtendedRouteTypes
|
|
) {
|
|
if (routes.some(p => p.gtfsType === ExtendedRouteTypes.BusExpress)) {
|
|
hasTrunkRoute = true;
|
|
}
|
|
}
|
|
if (
|
|
feature.properties.type === 'TRAM' &&
|
|
this.config.useExtendedRouteTypes
|
|
) {
|
|
if (routes.some(p => p.gtfsType === ExtendedRouteTypes.SpeedTram)) {
|
|
hasLocalTramRoute = true;
|
|
}
|
|
}
|
|
const ignoreMinZoomLevel =
|
|
feature.properties.type === 'FERRY' ||
|
|
feature.properties.type === 'RAIL' ||
|
|
feature.properties.type === 'SUBWAY';
|
|
if (ignoreMinZoomLevel || zoom >= minZoom) {
|
|
if (isHybrid) {
|
|
drawHybridStopIcon(
|
|
this.tile,
|
|
feature.geom,
|
|
isHighlighted,
|
|
this.config.colors.iconColors,
|
|
hasTrunkRoute,
|
|
);
|
|
return;
|
|
}
|
|
|
|
let mode = feature.properties.type;
|
|
if (hasTrunkRoute) {
|
|
mode = 'bus-express';
|
|
}
|
|
if (hasLocalTramRoute) {
|
|
mode = 'speedtram';
|
|
}
|
|
const stopOutOfService =
|
|
!!feature.properties.closedByServiceAlert ||
|
|
(feature.properties.servicesRunningInFuture === false &&
|
|
feature.properties.servicesRunningOnServiceDate === false); // if there are services added for the current day via realtime, servicesRunningOnServiceDate will be true
|
|
const noServiceOnServiceDay =
|
|
feature.properties.servicesRunningOnServiceDate === false;
|
|
|
|
if (isHighlighted && zoom <= minZoom) {
|
|
// Fetch stop details only when stop is highlighted and realtime layer is not used (zoom level)
|
|
this.drawHighlighted(
|
|
feature,
|
|
mode,
|
|
isHighlighted,
|
|
noServiceOnServiceDay,
|
|
stopOutOfService,
|
|
);
|
|
} else {
|
|
drawStopIcon(
|
|
this.tile,
|
|
feature.geom,
|
|
mode,
|
|
!isNull(feature.properties.platform)
|
|
? feature.properties.platform
|
|
: false,
|
|
isHighlighted,
|
|
!!(
|
|
feature.properties.type === 'FERRY' &&
|
|
!isNull(feature.properties.code)
|
|
),
|
|
this.config.colors.iconColors,
|
|
stopOutOfService,
|
|
noServiceOnServiceDay,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
stopsToShowCheck(feature, isStation) {
|
|
const feedid = feature.properties.gtfsId.split(':')[0];
|
|
if (!isStation && !this.config.feedIds.includes(feedid)) {
|
|
return false;
|
|
}
|
|
if (this.tile.stopsToShow) {
|
|
return this.tile.stopsToShow.includes(feature.properties.gtfsId);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
getPromise(lang) {
|
|
const zoomWithOffset =
|
|
this.tile.coords.z + (this.tile.props.zoomOffset || 0);
|
|
const stopsUrl =
|
|
zoomWithOffset >= this.config.stopsMinZoom
|
|
? this.config.URL.REALTIME_STOP_MAP
|
|
: this.config.URL.STOP_MAP;
|
|
return fetchWithLanguageAndSubscription(
|
|
`${getLayerBaseUrl(stopsUrl, lang)}${zoomWithOffset}/${
|
|
this.tile.coords.x
|
|
}/${this.tile.coords.y}.pbf`,
|
|
this.config,
|
|
lang,
|
|
).then(res => {
|
|
if (res.status !== 200) {
|
|
return undefined;
|
|
}
|
|
|
|
return res.arrayBuffer().then(
|
|
buf => {
|
|
const vt = new VectorTile(new Protobuf(buf));
|
|
this.features = [];
|
|
|
|
// draw highlighted stops on lower zoom levels
|
|
const hasHighlightedStops = !!(
|
|
this.tile.highlightedStops &&
|
|
this.tile.highlightedStops.length &&
|
|
this.tile.highlightedStops[0]
|
|
);
|
|
const stopLayer = vt.layers.stops || vt.layers.realtimeStops;
|
|
|
|
if (
|
|
stopLayer != null &&
|
|
(this.tile.coords.z >= this.config.stopsMinZoom ||
|
|
hasHighlightedStops)
|
|
) {
|
|
const featureByCode = {};
|
|
const hybridGtfsIdByCode = {};
|
|
const drawPlatforms =
|
|
this.config.terminalStopsMaxZoom - 1 <= zoomWithOffset;
|
|
const drawRailPlatforms =
|
|
this.config.railPlatformsMinZoom <= zoomWithOffset;
|
|
for (let i = 0, ref = stopLayer.length - 1; i <= ref; i++) {
|
|
const feature = stopLayer.feature(i);
|
|
if (
|
|
isFeatureLayerEnabled(feature, 'stop', this.mapLayers) &&
|
|
feature.properties.type &&
|
|
(isNull(feature.properties.parentStation) ||
|
|
drawPlatforms ||
|
|
(feature.properties.type === 'RAIL' && drawRailPlatforms))
|
|
) {
|
|
[[feature.geom]] = feature.loadGeometry();
|
|
const f = pick(feature, ['geom', 'properties']);
|
|
|
|
if (
|
|
// if under zoom level limit, only draw highlighted stops on near you page
|
|
this.tile.coords.z < this.config.stopsMinZoom &&
|
|
!(
|
|
hasHighlightedStops &&
|
|
this.tile.highlightedStops.includes(f.properties.gtfsId)
|
|
)
|
|
) {
|
|
continue; // eslint-disable-line no-continue
|
|
}
|
|
if (
|
|
f.properties.code &&
|
|
this.mergeStops &&
|
|
this.config.mergeStopsByCode
|
|
) {
|
|
/* a stop may be represented multiple times in data, once for each transport mode
|
|
Latest stop erares underlying ones unless the stop marker size is adjusted accordingly.
|
|
Currently we expand the first marker so that double stops are visialized nicely.
|
|
*/
|
|
const prevFeature = featureByCode[f.properties.code];
|
|
if (!prevFeature) {
|
|
featureByCode[f.properties.code] = f;
|
|
} else if (
|
|
this.config.mergeStopsByCode &&
|
|
f.properties.code &&
|
|
prevFeature.properties.type !== f.properties.type &&
|
|
f.geom.x === prevFeature.geom.x &&
|
|
f.geom.y === prevFeature.geom.y
|
|
) {
|
|
// save only one gtfsId per hybrid stop, always save the gtfsId for the bus stop to fetch extended route types
|
|
const featWithBus =
|
|
prevFeature.properties.type === 'BUS' ? prevFeature : f;
|
|
const featWithoutBus =
|
|
prevFeature.properties.type === 'BUS' ? f : prevFeature;
|
|
hybridGtfsIdByCode[featWithBus.properties.code] =
|
|
featWithBus.properties.gtfsId;
|
|
// Also change highlighted stopId to the stop with type = BUS in hybrid stop cases
|
|
if (
|
|
this.tile.highlightedStops &&
|
|
this.tile.highlightedStops.includes(
|
|
featWithoutBus.properties.gtfsId,
|
|
)
|
|
) {
|
|
this.tile.highlightedStops = [
|
|
featWithBus.properties.gtfsId,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
if (this.stopsToShowCheck(f, false)) {
|
|
this.features.push(f);
|
|
}
|
|
}
|
|
}
|
|
// sort to draw in correct order
|
|
this.features
|
|
.sort((a, b) => a.geom.y - b.geom.y)
|
|
.forEach(f => {
|
|
/* Note: don't expand separate stops sharing the same code,
|
|
unless type is different and location actually overlaps. */
|
|
const hybridId = hybridGtfsIdByCode[f.properties.code];
|
|
const draw = !hybridId || hybridId === f.properties.gtfsId;
|
|
if (draw) {
|
|
this.drawStop(
|
|
f,
|
|
!!hybridId,
|
|
this.tile.coords.z,
|
|
this.config.stopsMinZoom,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
if (
|
|
vt.layers.stations != null &&
|
|
this.config.terminalStopsMaxZoom > zoomWithOffset
|
|
) {
|
|
for (
|
|
let i = 0, ref = vt.layers.stations.length - 1;
|
|
i <= ref;
|
|
i++
|
|
) {
|
|
const feature = vt.layers.stations.feature(i);
|
|
const featureTypes = feature.properties.type.split(',');
|
|
const isHybridStation = featureTypes.length > 1 && false; // disable until we get proper icon
|
|
if (
|
|
feature.properties.type &&
|
|
isFeatureLayerEnabled(
|
|
feature,
|
|
'terminal',
|
|
this.mapLayers,
|
|
isHybridStation,
|
|
) &&
|
|
this.stopsToShowCheck(feature, true)
|
|
) {
|
|
[[feature.geom]] = feature.loadGeometry();
|
|
const isHighlighted =
|
|
this.tile.highlightedStops &&
|
|
this.tile.highlightedStops.includes(
|
|
feature.properties.gtfsId,
|
|
);
|
|
this.features.unshift(pick(feature, ['geom', 'properties']));
|
|
if (
|
|
isHybridStation &&
|
|
(isHighlighted ||
|
|
this.tile.coords.z >= this.config.terminalStopsMinZoom)
|
|
) {
|
|
drawHybridStationIcon(
|
|
this.tile,
|
|
feature.geom,
|
|
isHighlighted,
|
|
this.config.colors.iconColors,
|
|
);
|
|
}
|
|
if (
|
|
!isHybridStation &&
|
|
(isHighlighted ||
|
|
this.tile.coords.z >= this.config.terminalStopsMinZoom) &&
|
|
shouldRenderTerminalIcon(
|
|
feature.properties.type,
|
|
window.location.pathname,
|
|
this.tile?.vehicles,
|
|
)
|
|
) {
|
|
drawTerminalIcon(
|
|
this.tile,
|
|
feature.geom,
|
|
feature.properties.type,
|
|
isHighlighted,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
err => console.log(err), // eslint-disable-line no-console
|
|
);
|
|
});
|
|
}
|
|
|
|
drawHighlighted = (
|
|
feature,
|
|
mode,
|
|
isHighlighted,
|
|
noServiceOnServiceDay,
|
|
stopOutOfService,
|
|
) => {
|
|
const date = DateTime.now();
|
|
const callback = ({ stop: result }) => {
|
|
if (result) {
|
|
drawStopIcon(
|
|
this.tile,
|
|
feature.geom,
|
|
mode,
|
|
!isNull(feature.properties.platform)
|
|
? feature.properties.platform
|
|
: false,
|
|
isHighlighted,
|
|
!!(
|
|
feature.properties.type === 'FERRY' &&
|
|
!isNull(feature.properties.code)
|
|
),
|
|
this.config.colors.iconColors,
|
|
stopOutOfService,
|
|
noServiceOnServiceDay,
|
|
);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
fetchQuery(
|
|
this.relayEnvironment,
|
|
stopAlertsQuery,
|
|
{ stopId: feature.properties.gtfsId, date },
|
|
{ force: true },
|
|
)
|
|
.toPromise()
|
|
.then(callback);
|
|
};
|
|
}
|
|
|
|
export default Stops;
|