Skip to content

Commit df6fbf6

Browse files
authored
Merge pull request #1262 from jboolean/maplibre
Initial MapLibre implementation frontend under flag
2 parents f2d796d + c0efb52 commit df6fbf6

File tree

12 files changed

+5885
-212
lines changed

12 files changed

+5885
-212
lines changed

frontend/package-lock.json

Lines changed: 534 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@
3232
"immer": "^9.0.16",
3333
"lodash": "^4.17.21",
3434
"mapbox-gl": "^2.4.1",
35+
"maplibre-gl": "^5.6.2",
3536
"netlify-identity-widget": "^1.9.2",
3637
"normalize.css": "^8.0.1",
38+
"pmtiles": "^4.3.0",
3739
"qs": "^6.11.0",
3840
"react": "^18.2.0",
3941
"react-autosuggest": "^10.1.0",
@@ -69,6 +71,7 @@
6971
"@types/lint-staged": "~13.3.0",
7072
"@types/lodash": "^4.17.13",
7173
"@types/mapbox-gl": "^1.13.0",
74+
"@types/maplibre-gl": "^1.13.2",
7275
"@types/mini-css-extract-plugin": "^2.5.1",
7376
"@types/netlify-identity-widget": "^1.9.3",
7477
"@types/prettier": "^2.7.1",
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import React from 'react';
2+
3+
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom';
4+
5+
import classnames from 'classnames';
6+
import mapboxgl from 'mapbox-gl';
7+
8+
import { MapInterface, MapProps } from './MapInterface';
9+
import * as overlays from './overlays';
10+
11+
export { OverlayId } from './overlays';
12+
13+
import { RouteComponentProps } from 'react-router';
14+
import stylesheet from './MainMap.less';
15+
16+
const MAPBOX_STYLE = __DEV__
17+
? 'mapbox://styles/julianboilen/ck5jrzrs11r1p1imia7qzjkm1/draft'
18+
: 'mapbox://styles/julianboilen/ck5jrzrs11r1p1imia7qzjkm1';
19+
20+
const PHOTO_LAYER = 'photos-1940s';
21+
22+
type PropsWithRouter = MapProps & RouteComponentProps<{ identifier?: string }>;
23+
24+
class MapBoxMap
25+
extends React.PureComponent<PropsWithRouter>
26+
implements MapInterface
27+
{
28+
private mapContainer: HTMLElement;
29+
private map: mapboxgl.Map;
30+
31+
componentDidMount(): void {
32+
const map = (this.map = new mapboxgl.Map({
33+
container: this.mapContainer,
34+
style: MAPBOX_STYLE,
35+
center: [-73.99397, 40.7093],
36+
zoom: 13.69,
37+
maxBounds: [
38+
[-74.25908989999999, 40.4773991], // SW
39+
[-73.70027209999999, 40.9175771], // NE
40+
],
41+
hash: true,
42+
}));
43+
44+
map.on('click', PHOTO_LAYER, (e) => {
45+
const { panOnClick } = this.props;
46+
if (panOnClick) map.panTo(e.lngLat);
47+
if (!e || !e.features) return;
48+
const feature = e.features[0];
49+
const identifier = feature.properties?.photoIdentifier as string;
50+
this.props.history.push({
51+
pathname: '/map/photo/' + identifier,
52+
hash: window.location.hash,
53+
});
54+
});
55+
56+
// Change the cursor to a pointer when the mouse is over the places layer.
57+
map.on('mouseenter', PHOTO_LAYER, () => {
58+
map.getCanvas().style.cursor = 'pointer';
59+
});
60+
61+
// Change it back to a pointer when it leaves.
62+
map.on('mouseleave', PHOTO_LAYER, () => {
63+
map.getCanvas().style.cursor = '';
64+
});
65+
66+
map.on('style.load', () => {
67+
overlays.installLayers(this.map, PHOTO_LAYER);
68+
69+
map.setLayoutProperty(PHOTO_LAYER + '-active', 'visibility', 'visible');
70+
71+
this.syncUI();
72+
});
73+
74+
// Added to remove layers outside the viewport, to make the attribution correct
75+
map.on('moveend', () => {
76+
if (!map.isStyleLoaded()) return;
77+
this.syncUI();
78+
});
79+
}
80+
81+
componentDidUpdate(prevProps: PropsWithRouter): void {
82+
// Update the conditional color expression to make the active dot a different color
83+
if (!this.map.isStyleLoaded()) {
84+
this.map.once('style.load', () => this.syncUI());
85+
}
86+
if (
87+
prevProps.match.params.identifier !==
88+
this.props.match.params.identifier ||
89+
prevProps.overlay !== this.props.overlay
90+
) {
91+
this.syncUI();
92+
}
93+
}
94+
95+
syncUI(): void {
96+
this.map.setFilter(PHOTO_LAYER + '-active', [
97+
'==',
98+
['get', 'photoIdentifier'],
99+
this.props.match.params.identifier || null,
100+
]);
101+
102+
overlays.setOverlay(this.map, this.props.overlay);
103+
}
104+
105+
componentWillUnmount(): void {
106+
if (this.map) this.map.remove();
107+
}
108+
109+
/**
110+
* Call if container has resized
111+
*/
112+
resize(): void {
113+
this.map.resize();
114+
}
115+
116+
goTo(center: mapboxgl.LngLatLike): void {
117+
this.map.easeTo({
118+
zoom: 17.5,
119+
center,
120+
});
121+
}
122+
123+
render(): React.ReactNode {
124+
const { className: propsClassName } = this.props;
125+
126+
return (
127+
<div
128+
className={classnames(stylesheet.map, propsClassName)}
129+
ref={(el) => (this.mapContainer = el)}
130+
/>
131+
);
132+
}
133+
}
134+
135+
// Simple wrapper to provide router context
136+
function withRouterRef(
137+
Component: typeof MapBoxMap
138+
): React.ForwardRefExoticComponent<
139+
MapProps & React.RefAttributes<MapInterface>
140+
> {
141+
return React.forwardRef<MapInterface, MapProps>(function WithRouterRef(
142+
props,
143+
ref
144+
) {
145+
const match = useRouteMatch();
146+
const location = useLocation();
147+
const history = useHistory();
148+
149+
const componentRef = React.useRef<MapBoxMap>(null);
150+
151+
React.useImperativeHandle(
152+
ref,
153+
() => ({
154+
goTo: (center: { lng: number; lat: number } | [number, number]) => {
155+
if (componentRef.current) {
156+
componentRef.current.goTo(center);
157+
}
158+
},
159+
resize: () => {
160+
if (componentRef.current) {
161+
componentRef.current.resize();
162+
}
163+
},
164+
}),
165+
[]
166+
);
167+
168+
return (
169+
<Component
170+
ref={componentRef}
171+
match={match}
172+
location={location}
173+
history={history}
174+
{...props}
175+
/>
176+
);
177+
});
178+
}
179+
180+
export default withRouterRef(MapBoxMap);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as overlays from './overlays';
2+
3+
export interface MapInterface {
4+
goTo(center: { lng: number; lat: number } | [number, number]): void;
5+
resize(): void;
6+
}
7+
8+
export interface MapProps {
9+
className?: string;
10+
panOnClick: boolean;
11+
overlay: overlays.OverlayId;
12+
}

0 commit comments

Comments
 (0)