Skip to content

Commit

Permalink
Merge pull request #9 from Vizzuality/SKY30-65-custom-basemap
Browse files Browse the repository at this point in the history
[SKY30-65]: custom basemap / mapboxgl migration
  • Loading branch information
agnlez authored Oct 19, 2023
2 parents 28ffc9d + 9fc034b commit 40248c0
Show file tree
Hide file tree
Showing 27 changed files with 451 additions and 594 deletions.
1 change: 1 addition & 0 deletions frontend/.env.default
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
NEXT_PUBLIC_API_URL=http://0.0.0.0:1337/api
NEXT_PUBLIC_MAPBOX_API_TOKEN=
7 changes: 1 addition & 6 deletions frontend/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ module.exports = {
ignorePatterns: ['src/types/generated/*'],
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': [
'warn',
{
additionalHooks: '(useRecoilCallback|useRecoilTransaction_UNSTABLE)',
},
],
'react-hooks/exhaustive-deps': ['warn'],
'no-console': [1, { allow: ['info', 'error'] }],
'react/jsx-props-no-spreading': [
'error',
Expand Down
14 changes: 6 additions & 8 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,17 @@
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"date-fns": "^2.30.0",
"jotai": "2.4.3",
"lucide-react": "^0.274.0",
"mapbox-gl": "npm:[email protected].0",
"maplibre-gl": "^3.3.1",
"next": "13.2.4",
"mapbox-gl": "2.15.0",
"next": "13.5.6",
"next-usequerystate": "1.8.4",
"orval": "6.18.1",
"postcss": "8.4.21",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-map-gl": "^7.1.5",
"recoil": "^0.7.7",
"recoil-sync": "^0.2.0",
"recoil-sync-next": "^0.0.5",
"rooks": "^7.14.1",
"react-map-gl": "7.1.6",
"rooks": "7.14.1",
"tailwind-merge": "^1.14.0",
"tailwindcss": "3.2.7",
"tailwindcss-animate": "^1.0.7"
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/components/layer-manager.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { FC } from 'react';

import { Layer, Source } from 'react-map-gl/maplibre';
import { useRecoilValue } from 'recoil';
import { Layer, Source } from 'react-map-gl';

import { LAYERS } from '@/constants/map';
import { layersAtom } from '@/store/map';
import { useSyncMapSettings } from '@/containers/map/sync-settings';

const LayerManager: FC = () => {
const layers = useRecoilValue(layersAtom);
const [{ layers = [] }] = useSyncMapSettings();

return (
<>
Expand Down
7 changes: 3 additions & 4 deletions frontend/src/components/map/attributions/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { FC, useMemo } from 'react';

import { AttributionControl } from 'react-map-gl/maplibre';
import { useRecoilValue } from 'recoil';
import { AttributionControl } from 'react-map-gl';

import { LAYERS } from '@/constants/map';
import { layersAtom } from '@/store/map';
import { useSyncMapSettings } from '@/containers/map/sync-settings';

const Attributions: FC = () => {
const activeLayers = useRecoilValue(layersAtom);
const [{ layers: activeLayers = [] }] = useSyncMapSettings();

const customAttributions = useMemo(() => {
return activeLayers
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/map/draw-controls/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useEffect, useMemo } from 'react';

import { useMap, FillLayer, LineLayer, CircleLayer } from 'react-map-gl';

import MapboxDraw from '@mapbox/mapbox-gl-draw';
import { Feature } from 'geojson';
import { IControl } from 'maplibre-gl';
import { useMap, FillLayer, LineLayer, CircleLayer } from 'react-map-gl/maplibre';
import { IControl } from 'mapbox-gl';
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';

// See https://github.com/mapbox/mapbox-gl-draw/blob/main/docs/EXAMPLES.md
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/map/draw-controls/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FC, useCallback, useMemo, useState } from 'react';

import { useAtom } from 'jotai';
import { Trash2 } from 'lucide-react';
import { useRecoilState } from 'recoil';

import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
import { Button } from '@/components/ui/button';
Expand All @@ -11,7 +11,7 @@ import { useMapboxDraw, UseMapboxDrawProps } from './hooks';

const DrawControls: FC = () => {
const [startedDrawing, setStartedDrawing] = useState(false);
const [drawState, setDrawState] = useRecoilState(drawStateAtom);
const [drawState, setDrawState] = useAtom(drawStateAtom);

const onCreate: UseMapboxDrawProps['onCreate'] = useCallback(
({ features }) => {
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/map/drawing/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { FC, useEffect } from 'react';

import { Source, Layer, LngLatBoundsLike, useMap } from 'react-map-gl';

import { bbox } from '@turf/turf';
import { Source, Layer, LngLatBoundsLike } from 'react-map-gl/maplibre';
import { useMap } from 'react-map-gl/maplibre';
import { useRecoilValue } from 'recoil';
import { useAtomValue } from 'jotai';

import { drawStateAtom } from '@/store/map';

import { DRAW_STYLES } from '../draw-controls/hooks';

const Drawing: FC = () => {
const { current: map } = useMap();
const { active, feature } = useRecoilValue(drawStateAtom);
const { active, feature } = useAtomValue(drawStateAtom);

useEffect(() => {
if (map && feature) {
Expand Down
165 changes: 68 additions & 97 deletions frontend/src/components/map/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { useEffect, useState, useCallback, FC } from 'react';

import 'maplibre-gl/dist/maplibre-gl.css';
import { Map as MapLibreMap, FitBoundsOptions } from 'maplibre-gl';
import ReactMapGL, {
ViewState,
ViewStateChangeEvent,
MapEvent,
LngLatBoundsLike,
} from 'react-map-gl/maplibre';
import { useRecoilState } from 'recoil';
import { useDebouncedValue } from 'rooks';
import ReactMapGL, { ViewState, ViewStateChangeEvent, MapEvent, useMap } from 'react-map-gl';

import { cn } from '@/lib/utils';
import { bboxAtom } from '@/store/map';
import { useDebounce } from 'rooks';

import { DEFAULT_VIEW_STATE } from './constants';
import type { MapProps } from './types';
import type { CustomMapProps } from './types';

// import env from '@/env.mjs';
import { cn } from '@/lib/utils';

export const Map: FC<MapProps> = ({
export const Map: FC<CustomMapProps> = ({
// * if no id is passed, react-map-gl will store the map reference in a 'default' key:
// * https://github.com/visgl/react-map-gl/blob/ecb27c8d02db7dd09d8104e8c2011bda6aed4b6f/src/components/use-map.tsx#L18
id = 'default',
Expand All @@ -26,101 +19,82 @@ export const Map: FC<MapProps> = ({
viewState,
constrainedAxis,
initialViewState,
bounds: externalBounds,
bounds,
onMapViewStateChange,
dragPan = true,
dragRotate = false,
scrollZoom = true,
doubleClickZoom = true,
onLoad: externalOnLoad,
...rest
}) => {
const [map, setMap] = useState<MapLibreMap | null>(null);
onLoad,
...mapboxProps
}: CustomMapProps) => {
/**
* REFS
*/
const { [id]: mapRef } = useMap();

const [loaded, setLoaded] = useState(false);
const [urlBbox, setUrlBbox] = useRecoilState(bboxAtom);
/**
* STATE
*/
const [localViewState, setLocalViewState] = useState<Partial<ViewState> | null>(
!initialViewState
? {
...DEFAULT_VIEW_STATE,
...viewState,
...(urlBbox ? { bounds: urlBbox as LngLatBoundsLike } : {}),
}
: null
);
const [debouncedLocalViewState] = useDebouncedValue(localViewState, 250);
// Whether a bounds animation is playing
const [isFlying, setFlying] = useState(false);
// Store the timeout to restore `isFlying` to `false`
const [flyingTimeout, setFlyingTimeout] = useState<number | null>(null);
// Store whether the map has been interacted with
const [mapInteractedWith, setMapInteractedWith] = useState(false);
const [loaded, setLoaded] = useState(false);

/**
* CALLBACKS
*/
const fitBounds = useCallback(
(bounds: LngLatBoundsLike, options?: FitBoundsOptions) => {
if (map && bounds) {
// Enable fly mode to avoid the map being interrupted during the bounds transition
setFlying(true);
const debouncedViewStateChange = useDebounce((_viewState: Partial<ViewState>) => {
if (onMapViewStateChange) onMapViewStateChange(_viewState);
}, 250);

map.fitBounds(bounds, options);
const handleFitBounds = useCallback(() => {
if (mapRef && bounds) {
const { bbox, options } = bounds;
// enabling fly mode avoids the map to be interrupted during the bounds transition
setFlying(true);

if (flyingTimeout) {
window.clearInterval(flyingTimeout);
}
setFlyingTimeout(window.setTimeout(() => setFlying(false), options?.duration ?? 0));
}
},
[flyingTimeout, map]
);
mapRef.fitBounds(
[
[bbox[0], bbox[1]],
[bbox[2], bbox[3]],
],
options
);
}
}, [bounds, mapRef]);

const onMove = useCallback(
const handleMapMove = useCallback(
({ viewState: _viewState }: ViewStateChangeEvent) => {
const newViewState = {
..._viewState,
latitude: constrainedAxis === 'y' ? localViewState?.latitude : _viewState.latitude,
longitude: constrainedAxis === 'x' ? localViewState?.longitude : _viewState.longitude,
};
setLocalViewState(newViewState);
setMapInteractedWith(true);
debouncedViewStateChange(newViewState);
},
[constrainedAxis, localViewState?.latitude, localViewState?.longitude]
[constrainedAxis, localViewState?.latitude, localViewState?.longitude, debouncedViewStateChange]
);

const onLoad = useCallback(
const handleMapLoad = useCallback(
(e: MapEvent) => {
setLoaded(true);
setMap(e.target);

if (externalOnLoad) {
externalOnLoad(e);
if (onLoad) {
onLoad(e);
}
},
[externalOnLoad]
[onLoad]
);

useEffect(() => {
if (map && externalBounds) {
const { bbox, options } = externalBounds;
fitBounds(
[
[bbox[0], bbox[1]],
[bbox[2], bbox[3]],
],
options
);
if (mapRef && bounds) {
handleFitBounds();
}
}, [map, externalBounds, fitBounds]);

// Restore the map's position from the URL i.e. set the map bounds based on what the URL contains
// until the user has interacted with the map
useEffect(() => {
if (map && !mapInteractedWith && urlBbox) {
fitBounds(urlBbox as LngLatBoundsLike, { animate: false });
}
}, [map, urlBbox, fitBounds, mapInteractedWith]);
}, [mapRef, bounds, handleFitBounds]);

useEffect(() => {
setLocalViewState((prevViewState) => ({
Expand All @@ -129,42 +103,39 @@ export const Map: FC<MapProps> = ({
}));
}, [viewState]);

// Store the map's position in the URL every time it changes after the user's interacted with the
// map
useEffect(() => {
if (map && mapInteractedWith) {
setUrlBbox(
map
.getBounds()
.toArray()
.flat()
.map((b) => parseFloat(b.toFixed(2))) as [number, number, number, number]
);
}
if (!bounds) return undefined;

const { options } = bounds;
const animationDuration = options?.duration || 0;
let timeoutId: number;

if (onMapViewStateChange) {
onMapViewStateChange(debouncedLocalViewState);
if (isFlying) {
timeoutId = window.setTimeout(() => {
setFlying(false);
}, animationDuration);
}
}, [debouncedLocalViewState, map, mapInteractedWith, onMapViewStateChange, setUrlBbox]);

return () => {
if (timeoutId) {
window.clearInterval(timeoutId);
}
};
}, [bounds, isFlying]);

return (
<div className={cn('relative z-0 h-full w-full', className)}>
<ReactMapGL
id={id}
initialViewState={initialViewState}
dragPan={!isFlying && dragPan}
dragRotate={!isFlying && dragRotate}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
scrollZoom={!isFlying && scrollZoom}
doubleClickZoom={!isFlying && doubleClickZoom}
onMove={onMove}
onLoad={onLoad}
attributionControl={false}
{...rest}
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_API_TOKEN}
onMove={handleMapMove}
onLoad={handleMapLoad}
mapStyle="mapbox://styles/skytruth/clnud2d3100nr01pl3b4icpyw"
{...mapboxProps}
{...localViewState}
>
{!!map && loaded && !!children && children(map)}
{!!mapRef && loaded && children}
</ReactMapGL>
</div>
);
Expand Down
Loading

0 comments on commit 40248c0

Please sign in to comment.