digitransit-ui/app/component/map/tile-layer/Stops.js
Vesa Meskanen 66f0ffdb09 fix: remove class usage in icon coloring
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.
2025-09-19 11:03:32 +03:00

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;