Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4491b05
refactor: convert IconMarker to a function component
vesameskanen Sep 15, 2026
6aed27a
refactor: convert LegMarker to a function component
vesameskanen Sep 15, 2026
4bdde4e
refactor: convert MarkerPopupBottom to a function component
vesameskanen Sep 15, 2026
334480a
refactor: convert GenericMarker to a function component
vesameskanen Sep 15, 2026
ebf569e
refactor: convert non-tile-layer VehicleMarker to a function component
vesameskanen Sep 15, 2026
0f6927c
refactor: convert Line to a function component
vesameskanen Sep 15, 2026
0df98c2
refactor: convert LocationPopup to a function component
vesameskanen Sep 15, 2026
9b64254
refactor: convert StopMarker to a function component
vesameskanen Sep 15, 2026
efb748a
refactor: convert SelectFromMap to a function component
vesameskanen Sep 15, 2026
4397fc7
refactor: convert TransitLegMarkers to a function component
vesameskanen Sep 15, 2026
0bf7164
refactor: convert MapWithTracking to a function component
vesameskanen Sep 15, 2026
eb6a5f4
refactor: remove pointless '= undefined' parameter defaults
vesameskanen Sep 15, 2026
8e12557
fix: merge LocationPopup loading/location state to avoid crash
vesameskanen Sep 15, 2026
5bd9fba
fix: keep MapWithTracking's exposed ref methods up to date
vesameskanen Sep 15, 2026
d72a7ab
Merge remote-tracking branch 'origin/v3' into refactor-map-class-comp…
vesameskanen Sep 15, 2026
230906b
chore: remove old context from TileLayerContainer
vesameskanen Sep 16, 2026
5de9670
Fix LegMarker background-color CSS var when route has no color
vesameskanen Sep 16, 2026
3e3a1ff
Avoid rendering invalid color attribute on stop markers
vesameskanen Sep 16, 2026
84ef41e
Extract engine-agnostic logic from Leaflet marker components
vesameskanen Sep 16, 2026
49eaa8c
Remove dead screen-reader-only markup from LegMarker
vesameskanen Sep 16, 2026
74f1cf6
Merge branch 'v3' into refactor-map-class-components
vesameskanen Sep 21, 2026
0ca6084
Merge remote-tracking branch 'origin/v3' into refactor-map-class-comp…
vesameskanen Sep 23, 2026
8f329fc
Merge branch 'v3' into refactor-map-class-components
vesameskanen Sep 24, 2026
3a57a9b
Update app/component/map/GenericMarker.jsx
vesameskanen Sep 24, 2026
8fdc75a
Update app/component/map/GenericMarker.jsx
vesameskanen Sep 24, 2026
df9922e
Remove redundant LegMarker wrapper
vesameskanen Sep 24, 2026
a0f6f52
fix: remove hard-coded unnecessary font styles
vesameskanen Sep 24, 2026
f5672c6
chore: remove unclear comment
vesameskanen Sep 24, 2026
20e8002
Add MapWithTracking unit coverage
vesameskanen Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/component/map/ClusterNumberMarker.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export default function ClusterNumberMarker({ position, number }, { config }) {
dominant-baseline="middle"
fill="#fff"
font-size="${radius * 0.8}px"
font-family="Gotham XNarrow A, Gotham Rounded A, Gotham Rounded B, Roboto Condensed, Roboto, Arial, sans-serif"
>
${number}
</text>
Expand Down
161 changes: 69 additions & 92 deletions app/component/map/GenericMarker.jsx
Original file line number Diff line number Diff line change
@@ -1,131 +1,108 @@
import isFunction from 'lodash/isFunction';
import PropTypes from 'prop-types';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { withLeaflet } from 'react-leaflet/es/context';
import Marker from 'react-leaflet/es/Marker';
import Popup from 'react-leaflet/es/Popup';
import { default as L } from 'leaflet';
import { configShape, locationShape } from '../../../utils/client/shapes';
import { locationShape } from '../../../utils/client/shapes';
import { useConfigContext } from '../../client/ConfigContext';

class GenericMarker extends React.Component {
static displayName = 'GenericMarker';
function GenericMarker({
shouldRender = () => true,
position,
getIcon,
renderName = false,
name = '',
maxWidth,
minWidth,
children,
leaflet,
onClick = () => {},
zIndexOffset,
}) {
const config = useConfigContext();
const [zoom, setZoom] = useState(() => leaflet.map.getZoom());

static contextTypes = {
config: configShape.isRequired,
};
useEffect(() => {
const onMapMove = () => setZoom(leaflet.map.getZoom());
leaflet.map.on('zoomend', onMapMove);
return () => leaflet.map.off('zoomend', onMapMove);
}, [leaflet.map]);

static propTypes = {
shouldRender: PropTypes.func,
position: locationShape.isRequired,
getIcon: PropTypes.func.isRequired,
renderName: PropTypes.bool,
name: PropTypes.string,
maxWidth: PropTypes.number,
minWidth: PropTypes.number,
children: PropTypes.node,
leaflet: PropTypes.shape({
map: PropTypes.shape({
getZoom: PropTypes.func.isRequired,
on: PropTypes.func.isRequired,
off: PropTypes.func.isRequired,
}).isRequired,
}).isRequired,
onClick: PropTypes.func,
zIndexOffset: PropTypes.number,
};

static defaultProps = {
shouldRender: () => true,
onClick: () => {},
renderName: false,
name: '',
maxWidth: undefined,
minWidth: undefined,
children: undefined,
zIndexOffset: undefined,
};

state = { zoom: this.props.leaflet.map.getZoom() };

componentDidMount() {
this.props.leaflet.map.on('zoomend', this.onMapMove);
if (isFunction(shouldRender) && !shouldRender(zoom)) {
return null;
}

componentWillUnmount() {
this.props.leaflet.map.off('zoomend', this.onMapMove);
}

onMapMove = () => this.setState({ zoom: this.props.leaflet.map.getZoom() });

getMarker = () => (
const marker = (
<Marker
position={{ lat: this.props.position.lat, lng: this.props.position.lon }}
icon={this.props.getIcon(this.state.zoom)}
onClick={this.props.onClick}
position={{ lat: position.lat, lng: position.lon }}
icon={getIcon(zoom)}
onClick={onClick}
keyboard={false}
zIndexOffset={this.props.zIndexOffset}
zIndexOffset={zIndexOffset}
>
{this.props.children && (
{children && (
<Popup
maxWidth={
this.props.maxWidth ||
this.context.config.map.genericMarker.popup.maxWidth
}
minWidth={
this.props.minWidth ||
this.context.config.map.genericMarker.popup.minWidth
}
maxWidth={maxWidth || config.map.genericMarker.popup.maxWidth}
minWidth={minWidth || config.map.genericMarker.popup.minWidth}
className="popup"
>
{this.props.children}
{children}
</Popup>
)}
</Marker>
);

getNameMarker() {
if (
!this.props.renderName ||
this.props.leaflet.map.getZoom() <
this.context.config.map.genericMarker.nameMarkerMinZoom
) {
return false;
}
return (
const nameMarker = renderName &&
leaflet.map.getZoom() >= config.map.genericMarker.nameMarkerMinZoom && (
<Marker
key={`${this.props.name}_text`}
key={`${name}_text`}
position={{
lat: this.props.position.lat,
lng: this.props.position.lon,
lat: position.lat,
lng: position.lon,
}}
interactive={false}
icon={L.divIcon({
html: `<div>${this.props.name}</div>`,
html: `<div>${name}</div>`,
className: 'popup',
iconSize: [150, 0],
iconAnchor: [-8, 7],
})}
keyboard={false}
zIndexOffset={this.props.zIndexOffset}
zIndexOffset={zIndexOffset}
/>
);
}

render() {
const { shouldRender } = this.props;
const { zoom } = this.state;
if (isFunction(shouldRender) && !shouldRender(zoom)) {
return null;
}

return (
<React.Fragment>
{this.getMarker()}
{this.getNameMarker()}
</React.Fragment>
);
}
return (
<>
{marker}
{nameMarker}
</>
);
}

GenericMarker.displayName = 'GenericMarker';

GenericMarker.propTypes = {
shouldRender: PropTypes.func,
position: locationShape.isRequired,
getIcon: PropTypes.func.isRequired,
renderName: PropTypes.bool,
name: PropTypes.string,
maxWidth: PropTypes.number,
minWidth: PropTypes.number,
children: PropTypes.node,
leaflet: PropTypes.shape({
map: PropTypes.shape({
getZoom: PropTypes.func.isRequired,
on: PropTypes.func.isRequired,
off: PropTypes.func.isRequired,
}).isRequired,
}).isRequired,
onClick: PropTypes.func,
zIndexOffset: PropTypes.number,
};

const leafletComponent = withLeaflet(GenericMarker);
export { leafletComponent as default, GenericMarker as Component };
72 changes: 36 additions & 36 deletions app/component/map/IconMarker.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import PropTypes from 'prop-types';
import React from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { default as L } from 'leaflet';
import Marker from 'react-leaflet/es/Marker';

/* eslint-disable no-underscore-dangle */
export default class IconMarker extends React.Component {
constructor(props, ...args) {
super(props, ...args);
const _this = this;
export default function IconMarker({ icon, zIndexOffset, children, ...rest }) {
const [div, setDiv] = useState(undefined);
const hasMounted = useRef(false);

this.Icon = L.Icon.extend({
// The leaflet icon instance is created once and kept stable for the
// lifetime of the component; subsequent icon prop changes are applied via
// icon.initialize() below, mirroring the previous componentDidUpdate.
const iconInstance = useMemo(() => {
const DivIcon = L.Icon.extend({
options: {
// @section
// @aka DivIcon options
Expand All @@ -27,45 +29,48 @@ export default class IconMarker extends React.Component {
},

createIcon(oldIcon) {
const div =
const newDiv =
oldIcon && oldIcon.tagName === 'DIV'
? oldIcon
: document.createElement('div');

_this.setState({ div });
setDiv(newDiv);

this._setIconStyles(div, 'icon');
// eslint-disable-next-line no-underscore-dangle
this._setIconStyles(newDiv, 'icon');

return div;
return newDiv;
},

createShadow() {
return null;
},
});

this.state = { icon: new this.Icon(props.icon) };
}
return new DivIcon(icon);
// Intentionally created only once (empty deps) - see comment above.
}, []);

componentDidUpdate() {
this.state.icon.initialize(this.props.icon);
}
useEffect(() => {
if (hasMounted.current) {
iconInstance.initialize(icon);
} else {
hasMounted.current = true;
}
}, [icon, iconInstance]);

render() {
return [
this.state.div &&
createPortal(this.props.icon.element, this.state.div, 'icon'),
<Marker
key="marker"
{...this.props}
icon={this.state.icon}
keyboard={false}
zIndexOffset={this.props.zIndexOffset}
>
{this.props.children}
</Marker>,
];
}
return [
div && createPortal(icon.element, div, 'icon'),
<Marker
key="marker"
{...rest}
icon={iconInstance}
keyboard={false}
zIndexOffset={zIndexOffset}
>
{children}
</Marker>,
];
}

IconMarker.propTypes = {
Expand All @@ -79,8 +84,3 @@ IconMarker.propTypes = {
zIndexOffset: PropTypes.number,
children: PropTypes.node,
};

IconMarker.defaultProps = {
zIndexOffset: undefined,
children: undefined,
};
Loading
Loading