digitransit-ui/app/component/map/tile-layer/VehicleRentalStations.js
2025-07-03 14:25:08 +03:00

205 lines
6.4 KiB
JavaScript

import { VectorTile } from '@mapbox/vector-tile';
import Protobuf from 'pbf';
import { graphql, fetchQuery } from 'react-relay';
import pick from 'lodash/pick';
import {
getMapIconScale,
drawCitybikeIcon,
drawSmallVehicleRentalMarker,
} from '../../../util/mapIconUtils';
import { showCitybikeNetwork } from '../../../util/modeUtils';
import {
getRentalNetworkConfig,
getRentalNetworkIcon,
getVehicleCapacity,
BIKEAVL_UNKNOWN,
} from '../../../util/vehicleRentalUtils';
import { fetchWithLanguageAndSubscription } from '../../../util/fetchUtils';
import { getLayerBaseUrl } from '../../../util/mapLayerUtils';
const query = graphql`
query VehicleRentalStationsQuery($id: String!) {
station: vehicleRentalStation(id: $id) {
availableVehicles {
total
}
operative
}
}
`;
const REALTIME_REFETCH_FREQUENCY = 60000; // 60 seconds
class VehicleRentalStations {
constructor(tile, config, mapLayers, relayEnvironment) {
this.tile = tile;
this.config = config;
this.relayEnvironment = relayEnvironment;
this.scaleratio = window.devicePixelRatio || 1;
this.citybikeImageSize =
20 * this.scaleratio * getMapIconScale(this.tile.coords.z);
this.availabilityImageSize =
14 * this.scaleratio * getMapIconScale(this.tile.coords.z);
this.timeOfLastFetch = undefined;
this.canHaveStationUpdates = true;
}
getPromise = lang => this.fetchAndDraw(lang);
fetchAndDraw = lang => {
const zoomedIn =
this.tile.coords.z > this.config.vehicleRental.cityBikeSmallIconZoom;
const baseUrl = zoomedIn
? getLayerBaseUrl(this.config.URL.REALTIME_RENTAL_STATION_MAP, lang)
: getLayerBaseUrl(this.config.URL.RENTAL_STATION_MAP, lang);
const tileUrl = `${baseUrl}${
this.tile.coords.z + (this.tile.props.zoomOffset || 0)
}/${this.tile.coords.x}/${this.tile.coords.y}.pbf`;
return fetchWithLanguageAndSubscription(tileUrl, this.config, lang)
.then(res => {
this.timeOfLastFetch = Date.now();
if (res.status !== 200) {
return undefined;
}
return res.arrayBuffer().then(
buf => {
const vt = new VectorTile(new Protobuf(buf));
this.features = [];
const layer =
vt.layers.rentalStations || vt.layers.realtimeRentalStations;
if (layer) {
for (let i = 0, ref = layer.length - 1; i <= ref; i++) {
const feature = layer.feature(i);
[[feature.geom]] = feature.loadGeometry();
// Must filter out stations that are not shown as there can be a large amount
// of invisible rental stations, which are often accidentally clicked
if (
this.shouldShowStation(
feature.properties.id,
feature.properties.network,
)
) {
this.features.push(pick(feature, ['geom', 'properties']));
}
}
}
if (this.features.length === 0) {
this.canHaveStationUpdates = false;
} else {
// if zoomed out and there is a highlighted station,
// this value will be later reset to true
this.canHaveStationUpdates = zoomedIn;
this.features.forEach(feature => this.draw(feature, zoomedIn));
}
},
err => console.log(err), // eslint-disable-line no-console
);
})
.catch(err => {
this.timeOfLastFetch = Date.now();
console.log(err); // eslint-disable-line no-console
});
};
draw = (feature, zoomedIn) => {
const { id, network, formFactors } = feature.properties;
const iconName = getRentalNetworkIcon(
getRentalNetworkConfig(network, this.config),
);
const isHighlighted = this.tile.highlightedStops?.includes(id);
if (zoomedIn) {
this.drawLargeIcon(feature, iconName, isHighlighted);
} else if (isHighlighted) {
this.canHaveStationUpdates = true;
this.drawHighlighted(feature, iconName);
} else {
this.drawSmallMarker(feature.geom, iconName, formFactors);
}
};
drawLargeIcon = (
{ geom, properties: { network, operative, vehiclesAvailable } },
iconName,
isHighlighted,
) => {
const citybikeCapacity = getVehicleCapacity(this.config, network);
drawCitybikeIcon(
this.tile,
geom,
operative,
vehiclesAvailable,
iconName,
citybikeCapacity !== BIKEAVL_UNKNOWN,
isHighlighted,
);
};
drawHighlighted = ({ geom, properties: { id, network } }, iconName) => {
const citybikeCapacity = getVehicleCapacity(this.config, network);
const callback = ({ station: result }) => {
if (result) {
drawCitybikeIcon(
this.tile,
geom,
result.operative,
result.availableVehicles.total,
iconName,
citybikeCapacity !== BIKEAVL_UNKNOWN,
true,
);
}
return this;
};
fetchQuery(this.relayEnvironment, query, { id }, { force: true })
.toPromise()
.then(callback);
};
drawSmallMarker = (geom, iconName, formFactor) => {
const citybikeIconColor =
iconName.includes('secondary') &&
this.config.colors.iconColors['mode-citybike-secondary']
? this.config.colors.iconColors['mode-citybike-secondary']
: this.config.colors.iconColors['mode-citybike'];
const iconColor =
formFactor === 'SCOOTER'
? this.config.colors.iconColors['mode-scooter']
: citybikeIconColor;
drawSmallVehicleRentalMarker(this.tile, geom, iconColor, formFactor);
};
onTimeChange = lang => {
const currentTime = Date.now();
if (
this.canHaveStationUpdates &&
(!this.timeOfLastFetch ||
currentTime - this.timeOfLastFetch > REALTIME_REFETCH_FREQUENCY)
) {
this.fetchAndDraw(lang);
}
};
shouldShowStation = (id, network) =>
this.config.vehicleRental.networks[network] &&
(this.config.vehicleRental.networks[network].showRentalStations ===
undefined ||
this.config.vehicleRental.networks[network].showRentalStations) &&
(!this.tile.stopsToShow || this.tile.stopsToShow.includes(id)) &&
!this.tile.objectsToHide.vehicleRentalStations.includes(id) &&
showCitybikeNetwork(this.config.vehicleRental.networks[network]);
static getName = () => 'citybike';
}
export default VehicleRentalStations;