mirror of
https://github.com/HSLdevcom/digitransit-ui
synced 2025-07-27 15:05:15 +02:00
235 lines
6.5 KiB
JavaScript
235 lines
6.5 KiB
JavaScript
import L from 'leaflet';
|
|
import PropTypes from 'prop-types';
|
|
import React from 'react';
|
|
import uniqBy from 'lodash/uniqBy';
|
|
import { default as Geojson } from 'react-leaflet/es/GeoJSON';
|
|
import PointFeatureMarker from './PointFeatureMarker';
|
|
import { configShape, geoJsonFeatureShape } from '../../util/shapes';
|
|
import {
|
|
isMultiPointTypeGeometry,
|
|
isPointTypeGeometry,
|
|
} from '../../util/geo-utils';
|
|
|
|
/**
|
|
* Extracts svg-formatted icon data from the given features' properties.
|
|
*
|
|
* @param {*[]} features the geojson features.
|
|
*/
|
|
const getIcons = features => {
|
|
if (!Array.isArray(features) || features.length === 0) {
|
|
return {};
|
|
}
|
|
|
|
return features
|
|
.filter(
|
|
feature => feature.properties?.icon?.id && feature.properties?.icon?.svg,
|
|
)
|
|
.map(feature => feature.properties.icon)
|
|
.reduce((icons, icon) => {
|
|
/*
|
|
For data URI SVG support in Firefox & IE it's necessary to URI encode the string
|
|
& replace the '#' character with '%23'. `encodeURI()` won't do this.
|
|
*/
|
|
const url = `data:image/svg+xml;charset=utf-8,${encodeURI(
|
|
icon.svg,
|
|
).replace(/#/g, '%23')}`;
|
|
icons[icon.id] = url; // eslint-disable-line no-param-reassign
|
|
return icons;
|
|
}, {});
|
|
};
|
|
|
|
/**
|
|
* Generates a suitable leaflet marker with a tooltip and a popup attached (if applicable)
|
|
* for the given feature.
|
|
*
|
|
* @param {*} feature the feature currently being manipulated.
|
|
* @param {*} latlng the coordinates for the current feature.
|
|
* @param {*} icons the custom icons collection, if available.
|
|
*/
|
|
const getMarker = (feature, latlng, icons = {}) => {
|
|
const properties = feature.properties || {};
|
|
const interactive = !!properties.popupContent;
|
|
let marker;
|
|
|
|
if (properties.icon) {
|
|
marker = L.marker(latlng, {
|
|
icon: new L.Icon({
|
|
iconUrl: icons[properties.icon.id],
|
|
className: 'icon-zone',
|
|
}),
|
|
interactive,
|
|
keyboard: false,
|
|
});
|
|
} else if (properties.textOnly) {
|
|
marker = L.circleMarker(latlng, {
|
|
interactive,
|
|
keyboard: false,
|
|
});
|
|
marker.bindTooltip(properties.textOnly, {
|
|
className: 'geoJsonText',
|
|
direction: 'center',
|
|
offset: [0, 0],
|
|
permanent: true,
|
|
});
|
|
} else {
|
|
marker = L.circleMarker(latlng, { interactive, keyboard: false });
|
|
}
|
|
|
|
return marker;
|
|
};
|
|
|
|
const addPopup = (feature, layer) => {
|
|
if (feature.properties?.popupContent) {
|
|
layer.bindPopup(feature.properties.popupContent, {
|
|
className: 'geoJsonPopup',
|
|
});
|
|
}
|
|
};
|
|
|
|
class GeoJSON extends React.Component {
|
|
static propTypes = {
|
|
// eslint-disable-next-line
|
|
bounds: PropTypes.object,
|
|
data: PropTypes.shape({
|
|
features: PropTypes.arrayOf(geoJsonFeatureShape),
|
|
}).isRequired,
|
|
geoJsonZoomLevel: PropTypes.number,
|
|
locationPopup: PropTypes.string,
|
|
onSelectLocation: PropTypes.func,
|
|
};
|
|
|
|
static defaultProps = {
|
|
bounds: undefined,
|
|
geoJsonZoomLevel: undefined,
|
|
locationPopup: undefined,
|
|
onSelectLocation: undefined,
|
|
};
|
|
|
|
static contextTypes = { config: configShape.isRequired };
|
|
|
|
// add some custom rendering control by feature props
|
|
pointToLayer = (feature, latlng) => getMarker(feature, latlng, this.icons);
|
|
|
|
styler = feature => {
|
|
const { config } = this.context;
|
|
const defaultLineStyle = {
|
|
className: 'cursor-grab',
|
|
color: config.colors.primary,
|
|
weight: 3,
|
|
opacity: 0.8,
|
|
};
|
|
const defaultMarkerStyle = {
|
|
color: config.colors.primary,
|
|
fillColor: 'white',
|
|
radius: 6,
|
|
opacity: 1,
|
|
fillOpacity: 1,
|
|
weight: 2,
|
|
};
|
|
const textMarkerStyle = {
|
|
color: config.colors.primary,
|
|
radius: 0,
|
|
opacity: 0,
|
|
fillOpacity: 0,
|
|
weight: 0,
|
|
};
|
|
|
|
const { geometry } = feature;
|
|
if (isPointTypeGeometry(geometry) || isMultiPointTypeGeometry(geometry)) {
|
|
if (feature.properties?.textOnly) {
|
|
return feature.style
|
|
? { ...textMarkerStyle, ...feature.style }
|
|
: textMarkerStyle;
|
|
}
|
|
return feature.style
|
|
? { ...defaultMarkerStyle, ...feature.style }
|
|
: defaultMarkerStyle;
|
|
}
|
|
|
|
if (
|
|
this.props.geoJsonZoomLevel &&
|
|
feature.style &&
|
|
(geometry.type === 'MultiLineString' || geometry.type === 'LineString')
|
|
) {
|
|
const lineArray = [
|
|
0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.55, 0.61, 0.69, 0.78, 0.89, 1.02, 1.17,
|
|
1.36, 1.58, 1.85, 2.17, 2.56, 3.02, 3.57, 4.24, 5.04, 6,
|
|
];
|
|
const haloArray = [
|
|
2, 2, 2, 2, 2, 2, 2.74, 3.62, 4.68, 5.95, 7.48, 9.31, 11.51, 14.05,
|
|
17.31, 21.11, 25.67, 31.14, 37.71, 45.59, 55.04, 66.39, 80,
|
|
];
|
|
|
|
const index =
|
|
this.props.geoJsonZoomLevel !== -1 ? this.props.geoJsonZoomLevel : 0;
|
|
const newStyle = {
|
|
...feature.style,
|
|
weight:
|
|
feature.style.type === 'halo' ? haloArray[index] : lineArray[index],
|
|
};
|
|
return { ...defaultLineStyle, ...newStyle };
|
|
}
|
|
|
|
return feature.style
|
|
? { ...defaultLineStyle, ...feature.style }
|
|
: defaultLineStyle;
|
|
};
|
|
|
|
// cache dynamic icons to allow references by id without data duplication
|
|
// eslint-disable-next-line camelcase
|
|
UNSAFE_componentWillMount() {
|
|
const {
|
|
data: { features },
|
|
} = this.props;
|
|
this.icons = getIcons(features);
|
|
}
|
|
|
|
render() {
|
|
const { bounds, data } = this.props;
|
|
if (!data || !Array.isArray(data.features)) {
|
|
return null;
|
|
}
|
|
|
|
const hasOnlyPointGeometries = data.features.every(feature =>
|
|
isPointTypeGeometry(feature.geometry),
|
|
);
|
|
if (!hasOnlyPointGeometries) {
|
|
return (
|
|
<Geojson
|
|
data={data}
|
|
pointToLayer={this.pointToLayer}
|
|
style={this.styler}
|
|
onEachFeature={addPopup}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<React.Fragment>
|
|
{uniqBy(data.features, 'id')
|
|
.filter(feature => {
|
|
const [lon, lat] = feature.geometry.coordinates;
|
|
if (bounds) {
|
|
const latLng = L.latLng({ lat, lng: lon });
|
|
if (!bounds.contains(latLng)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
})
|
|
.map(feature => (
|
|
<PointFeatureMarker
|
|
feature={feature}
|
|
icons={this.icons}
|
|
key={feature.id}
|
|
locationPopup={this.props.locationPopup}
|
|
onSelectLocation={this.props.onSelectLocation}
|
|
size={this.context.config.geoJsonSvgSize}
|
|
/>
|
|
))}
|
|
</React.Fragment>
|
|
);
|
|
}
|
|
}
|
|
|
|
export { GeoJSON as default, getIcons, getMarker };
|