this.routeFrom()} className="route cursor-pointer">
+ return (
+
+
routeFrom()} className="route cursor-pointer">
+
+
+ {locationPopup === 'all' && (
+
routeAddViaPoint()}
+ className="route cursor-pointer route-add-viapoint"
+ >
- {this.props.locationPopup === 'all' && (
-
this.routeAddViaPoint()}
- className="route cursor-pointer route-add-viapoint"
- >
-
-
- )}
-
this.routeTo()} className="route cursor-pointer">
-
-
+ )}
+
routeTo()} className="route cursor-pointer">
+
- );
- }
+
+ );
}
+MarkerPopupBottom.displayName = 'MarkerPopupBottom';
+
+MarkerPopupBottom.propTypes = {
+ location: locationShape.isRequired,
+ leaflet: PropTypes.shape({
+ map: PropTypes.shape({
+ closePopup: PropTypes.func.isRequired,
+ }).isRequired,
+ }).isRequired,
+ onSelectLocation: PropTypes.func.isRequired,
+ locationPopup: PropTypes.string,
+};
+
const markerPopupBottomWithLeaflet = withLeaflet(MarkerPopupBottom);
export {
diff --git a/app/component/map/SelectFromMap.jsx b/app/component/map/SelectFromMap.jsx
index ba4a6f93c9..8c649b3f9f 100644
--- a/app/component/map/SelectFromMap.jsx
+++ b/app/component/map/SelectFromMap.jsx
@@ -1,10 +1,10 @@
import PropTypes from 'prop-types';
-import React from 'react';
+import React, { useRef, useState } from 'react';
import get from 'lodash/get';
-import { matchShape } from 'found';
+import { useRouter } from 'found';
import connectToStores from 'fluxible-addons-react/connectToStores';
+import { useIntl } from 'react-intl';
import getLabel from '@digitransit-search-util/digitransit-search-util-get-label';
-import { configShape } from '../../../utils/client/shapes';
import LocationMarker from './LocationMarker';
import MapWithTracking from './MapWithTracking';
import { otpToLocation } from '../../../utils/shared/otpStrings';
@@ -13,6 +13,7 @@ import { mapLayerShape } from '../../store/MapLayerStore';
import withBreakpoint from '../../../utils/client/withBreakpoint';
import LocationMarkerWithPermanentTooltip from './LocationMarkerWithPermanentTooltip';
import ConfirmLocationFromMapButton from './ConfirmLocationFromMapButton';
+import { useConfigContext } from '../../client/ConfigContext';
const DESKTOP_BREAKPOINT = 'large';
@@ -37,135 +38,107 @@ const markLocation = (markerType, position) => {
return null;
};
-class SelectFromMap extends React.Component {
- static contextTypes = {
- match: matchShape,
- config: configShape,
- intl: PropTypes.object,
- };
-
- static propTypes = {
- breakpoint: PropTypes.string,
- language: PropTypes.string,
- type: PropTypes.string.isRequired,
- onConfirm: PropTypes.func.isRequired,
- mapLayers: mapLayerShape.isRequired,
- };
-
- static defaultProps = {
- breakpoint: undefined,
- language: undefined,
- };
-
- constructor(props) {
- super(props);
- this.state = {};
- }
+function SelectFromMap({ breakpoint, language, type, onConfirm, mapLayers }) {
+ const config = useConfigContext();
+ const intl = useIntl();
+ const { match } = useRouter();
+ const map = useRef(null);
+ const [mapCenter, setMapCenter] = useState(undefined);
- setMapElementRef = element => {
- this.map = get(element, 'leafletElement', null);
+ const setMapElementRef = element => {
+ map.current = get(element, 'leafletElement', null);
};
- setAddress = (lat, lon) => {
- const { intl } = this.context;
-
+ const setAddress = (lat, lon) => {
const searchParams = {
'point.lat': lat,
'point.lon': lon,
'boundary.circle.radius': 0.1, // 100m
- lang: this.props.language,
+ lang: language,
size: 1,
layers: 'address',
zones: 1,
};
- if (this.context.config.searchParams['boundary.country']) {
+ if (config.searchParams['boundary.country']) {
searchParams['boundary.country'] =
- this.context.config.searchParams['boundary.country'];
+ config.searchParams['boundary.country'];
}
- getJson(this.context.config.URL.PELIAS_REVERSE_GEOCODER, searchParams).then(
+ getJson(config.URL.PELIAS_REVERSE_GEOCODER, searchParams).then(
data => {
if (data.features != null && data.features.length > 0) {
- const match = data.features[0].properties;
- this.setState(prevState => ({
- mapCenter: {
- ...prevState.mapCenter,
- address: getLabel(match),
- lat,
- lon,
- onlyCoordinates: false,
- },
+ const { properties } = data.features[0];
+ setMapCenter(prevMapCenter => ({
+ ...prevMapCenter,
+ address: getLabel(properties),
+ lat,
+ lon,
+ onlyCoordinates: false,
}));
} else {
- this.setState(prevState => ({
- mapCenter: {
- ...prevState.mapCenter,
- address: intl.formatMessage({
- id: 'location-from-map',
- defaultMessage: 'Selected location',
- }), // + ', ' + JSON.stringify(centerOfMap.lat).match(/[0-9]{1,3}.[0-9]{6}/) + ' ' + JSON.stringify(centerOfMap.lng).match(/[0-9]{1,3}.[0-9]{6}/),
- lat,
- lon,
- onlyCoordinates: true,
- },
- }));
- }
- },
- () => {
- this.setState({
- mapCenter: {
+ setMapCenter(prevMapCenter => ({
+ ...prevMapCenter,
address: intl.formatMessage({
id: 'location-from-map',
defaultMessage: 'Selected location',
- }), // + ', ' + JSON.stringify(centerOfMap.lat).match(/[0-9]{1,3}.[0-9]{6}/) + ' ' + JSON.stringify(centerOfMap.lng).match(/[0-9]{1,3}.[0-9]{6}/),
+ }),
lat,
lon,
onlyCoordinates: true,
- },
+ }));
+ }
+ },
+ () => {
+ setMapCenter({
+ address: intl.formatMessage({
+ id: 'location-from-map',
+ defaultMessage: 'Selected location',
+ }),
+ lat,
+ lon,
+ onlyCoordinates: true,
});
},
);
};
- onClick = e => {
+ const onClick = e => {
const clickedDiv = e.originalEvent.target;
if (clickedDiv.tagName === 'BUTTON') {
return;
}
- this.setState({
- mapCenter: {
- address: '',
- lat: e.latlng.lat,
- lon: e.latlng.lng,
- },
+ setMapCenter({
+ address: '',
+ lat: e.latlng.lat,
+ lon: e.latlng.lng,
});
- this.setAddress(e.latlng.lat, e.latlng.lng);
+ setAddress(e.latlng.lat, e.latlng.lng);
};
- setMapLocation = () => {
- if (!this.map) {
+ const setMapLocation = () => {
+ if (!map.current) {
return;
}
- const centerOfMap = this.map.getCenter();
+ const centerOfMap = map.current.getCenter();
if (
- this.state.mapCenter &&
- this.state.mapCenter.lat === centerOfMap.lat &&
- this.state.mapCenter.lon === centerOfMap.lng
+ mapCenter &&
+ mapCenter.lat === centerOfMap.lat &&
+ mapCenter.lon === centerOfMap.lng
) {
return;
}
- this.setAddress(centerOfMap.lat, centerOfMap.lng);
+ setAddress(centerOfMap.lat, centerOfMap.lng);
};
- createAddress = (address, position) => {
+ const createAddress = (address, position) => {
if (address !== '') {
const newAddress = address.split(', ');
let strippedAddress = newAddress[0];
- if (!this.state.mapCenter.onlyCoordinates) {
+ if (!mapCenter.onlyCoordinates) {
strippedAddress = `${strippedAddress}, ${newAddress[1]}`;
}
strippedAddress = `${strippedAddress}::${JSON.stringify(
@@ -176,125 +149,122 @@ class SelectFromMap extends React.Component {
return '';
};
- confirmButton = (isEnabled, mapCenter, positionSelectingFromMap) => {
- const { intl, config } = this.context;
+ const confirmButton = (isEnabled, center, positionSelectingFromMap) => (
+
+ );
- return (
-
- );
- };
+ const defaultLocation = config.defaultEndpoint;
+ const isDesktop = breakpoint === DESKTOP_BREAKPOINT;
- render() {
- const { config, match } = this.context;
- const { type } = this.props;
- const { mapCenter } = this.state;
- const defaultLocation = config.defaultEndpoint;
- const isDesktop = this.props.breakpoint === DESKTOP_BREAKPOINT;
+ const leafletObjs = [];
- const leafletObjs = [];
+ if (!mapCenter && type === 'origin' && !isDesktop) {
+ leafletObjs.push(
+
,
+ );
+ }
- if (!mapCenter && type === 'origin' && !isDesktop) {
- leafletObjs.push(
-
,
- );
- }
+ if (!mapCenter && type === 'destination' && !isDesktop) {
+ leafletObjs.push(
+
,
+ );
+ }
- if (!mapCenter && type === 'destination' && !isDesktop) {
+ if (match.location.query && match.location.query.intermediatePlaces) {
+ if (Array.isArray(match.location.query.intermediatePlaces)) {
+ match.location.query.intermediatePlaces
+ .map(otpToLocation)
+ .forEach((markerLocation, i) => {
+ leafletObjs.push(
+
,
+ );
+ });
+ } else {
leafletObjs.push(
,
);
}
+ }
- if (match.location.query && match.location.query.intermediatePlaces) {
- if (Array.isArray(match.location.query.intermediatePlaces)) {
- match.location.query.intermediatePlaces
- .map(otpToLocation)
- .forEach((markerLocation, i) => {
- leafletObjs.push(
-
,
- );
- });
- } else {
- leafletObjs.push(
-
,
- );
- }
- }
-
- const positionSelectingFromMap = mapCenter || defaultLocation;
+ const positionSelectingFromMap = mapCenter || defaultLocation;
- if (!mapCenter) {
- leafletObjs.push(this.confirmButton(false));
- } else {
- leafletObjs.push(markLocation(this.props.type, positionSelectingFromMap));
- leafletObjs.push(
-
,
- );
- leafletObjs.push(
- this.confirmButton(true, mapCenter, positionSelectingFromMap),
- );
- }
- const eventHooks = {};
- if (isDesktop) {
- eventHooks.leafletEvents = {
- onClick: this.onClick,
- };
- } else {
- eventHooks.onEndNavigation = this.setMapLocation;
- }
-
- return (
-
+ if (!mapCenter) {
+ leafletObjs.push(confirmButton(false));
+ } else {
+ leafletObjs.push(markLocation(type, positionSelectingFromMap));
+ leafletObjs.push(
+
,
);
+ leafletObjs.push(confirmButton(true, mapCenter, positionSelectingFromMap));
}
+ const eventHooks = {};
+ if (isDesktop) {
+ eventHooks.leafletEvents = {
+ onClick,
+ };
+ } else {
+ eventHooks.onEndNavigation = setMapLocation;
+ }
+
+ return (
+
+ );
}
+SelectFromMap.propTypes = {
+ breakpoint: PropTypes.string,
+ language: PropTypes.string,
+ type: PropTypes.string.isRequired,
+ onConfirm: PropTypes.func.isRequired,
+ mapLayers: mapLayerShape.isRequired,
+};
+
export default connectToStores(
withBreakpoint(SelectFromMap),
['MapLayerStore'],
diff --git a/app/component/map/non-tile-layer/LegMarker.jsx b/app/component/map/non-tile-layer/LegMarker.jsx
index d96b0d7b61..ae39e90ab9 100644
--- a/app/component/map/non-tile-layer/LegMarker.jsx
+++ b/app/component/map/non-tile-layer/LegMarker.jsx
@@ -4,85 +4,85 @@ import Marker from 'react-leaflet/es/Marker';
import { default as L } from 'leaflet';
import cx from 'classnames';
import Icon from '../../Icon';
-import { legShape, configShape } from '../../../../utils/client/shapes';
+import { legShape } from '../../../../utils/client/shapes';
import { renderAsString } from '../../../../utils/client/mapIconUtils';
+import { useConfigContext } from '../../../client/ConfigContext';
-class LegMarker extends React.Component {
- static propTypes = {
- leg: legShape.isRequired,
- mode: PropTypes.string.isRequired,
- color: PropTypes.string,
- zIndexOffset: PropTypes.number,
- wide: PropTypes.bool,
- style: PropTypes.string,
- appendClass: PropTypes.string,
- };
+// The functions below compute plain values (icon name, route number markup,
+// visibility) with no Leaflet dependency. They are exported for unit testing
+// and can be reused as-is if the underlying map engine changes.
+export const getLegMarkerIconName = mode =>
+ mode === 'bus-express' ? 'icon_bus' : `icon_${mode}`;
- static defaultProps = {
- color: 'currentColor',
- zIndexOffset: undefined,
- wide: false,
- style: undefined,
- appendClass: undefined,
- };
+// Do not display route number if it is an external route and the route number is empty.
+export const shouldDisplayLegRouteNumber = (config, mode, legName) =>
+ !(
+ config.externalFeedIds !== undefined &&
+ mode.includes('external') &&
+ legName === ''
+ );
- static contextTypes = {
- config: configShape.isRequired,
- };
+export const getLegRouteNumberHtml = (mode, legName, displayRouteNumber) =>
+ displayRouteNumber
+ ? `
${legName}`
+ : '';
- // An arrow marker will be displayed if the normal marker can't fit
- getLegMarker() {
- const color = this.props.color ? this.props.color : 'currentColor';
- const className = this.props.wide ? 'wide' : '';
- const iconName =
- this.props.mode === 'bus-express'
- ? 'icon_bus'
- : `icon_${this.props.mode}`;
- // Do not display route number if it is an external route and the route number is empty.
- const displayRouteNumber = !(
- this.context.config.externalFeedIds !== undefined &&
- this.props.mode.includes('external') &&
- this.props.leg.name === ''
- );
- const routeNumber = displayRouteNumber
- ? `
${
- this.props.leg.name
- }
-
${this.props.leg.name.toLowerCase()}`
- : '';
- return (
-
- ${renderAsString(
- ,
- )}
- ${routeNumber}
- `,
- className: cx(
- this.props.style ? `arrow-${this.props.style}` : 'legmarker',
- this.props.mode,
- { 'only-icon': !displayRouteNumber },
- this.props.appendClass,
- ),
- iconSize: null,
- })}
- zIndexOffset={this.props.zIndexOffset}
- keyboard={false}
- />
- );
- }
+// An arrow marker will be displayed if the normal marker can't fit
+export default function LegMarker({
+ leg,
+ mode,
+ color = 'currentColor',
+ zIndexOffset,
+ wide = false,
+ style,
+ appendClass,
+}) {
+ const config = useConfigContext();
+ const className = wide ? 'wide' : '';
+ const iconName = getLegMarkerIconName(mode);
+ const displayRouteNumber = shouldDisplayLegRouteNumber(
+ config,
+ mode,
+ leg.name,
+ );
+ const routeNumber = getLegRouteNumberHtml(mode, leg.name, displayRouteNumber);
- render() {
- return