mirror of
https://github.com/HSLdevcom/digitransit-ui
synced 2025-10-02 21:06:29 +02:00

Icon coloring was a confusing mixture of fill with current color and style class defined colors. Now icons expect that fill color is the current color.
406 lines
14 KiB
JavaScript
406 lines
14 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';
|
|
|
|
hasSpeedTram(feature, routes) {
|
|
if (
|
|
feature.properties.type === 'TRAM' &&
|
|
this.config.useExtendedRouteTypes
|
|
) {
|
|
return routes.some(p => p.gtfsType === ExtendedRouteTypes.SpeedTram);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
hasTrunkRoute(feature, routes) {
|
|
if (
|
|
feature.properties.type === 'BUS' &&
|
|
this.config.useExtendedRouteTypes
|
|
) {
|
|
return routes.some(p => p.gtfsType === ExtendedRouteTypes.BusExpress);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
drawStop(feature, isHybrid, zoom, minZoom) {
|
|
const isHighlighted =
|
|
this.tile.highlightedStops &&
|
|
this.tile.highlightedStops.includes(feature.properties.gtfsId);
|
|
|
|
const routes = JSON.parse(feature.properties.routes);
|
|
const hasSpeedTram = this.hasSpeedTram(feature, routes);
|
|
const hasTrunkRoute = this.hasTrunkRoute(feature, routes);
|
|
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';
|
|
} else if (hasSpeedTram) {
|
|
mode = 'speedtram';
|
|
}
|
|
const stopOutOfService =
|
|
this.config.showStopStatusMarkers &&
|
|
(!!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 =
|
|
this.config.showStopStatusMarkers &&
|
|
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,
|
|
)
|
|
) {
|
|
const routes = JSON.parse(feature.properties.routes);
|
|
const type = this.hasSpeedTram(feature, routes)
|
|
? 'speedtram'
|
|
: feature.properties.type;
|
|
drawTerminalIcon(
|
|
this.tile,
|
|
feature.geom,
|
|
type,
|
|
isHighlighted,
|
|
this.config.colors.iconColors,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
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;
|