Skip to content

Commit

Permalink
Merge pull request #30
Browse files Browse the repository at this point in the history
Mask the layers by the selected location
  • Loading branch information
clementprdhomme authored Nov 19, 2024
2 parents 06308c1 + b1c4da9 commit a207052
Show file tree
Hide file tree
Showing 20 changed files with 689 additions and 98 deletions.
3 changes: 3 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@dnd-kit/core": "6.1.0",
"@dnd-kit/modifiers": "7.0.0",
"@dnd-kit/sortable": "8.0.0",
"@loaders.gl/gis": "4.3.2",
"@radix-ui/react-checkbox": "1.1.2",
"@radix-ui/react-collapsible": "1.1.1",
"@radix-ui/react-dialog": "1.1.2",
Expand All @@ -39,6 +40,8 @@
"@t3-oss/env-nextjs": "0.11.1",
"@tanstack/react-query": "5.59.16",
"@turf/bbox": "7.1.0",
"@turf/helpers": "7.1.0",
"@turf/meta": "7.1.0",
"@types/mapbox-gl": "3.4.0",
"apng-js": "1.1.4",
"axios": "1.7.7",
Expand Down
16 changes: 12 additions & 4 deletions client/src/components/map/deckgl-mapbox-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,24 @@ const DeckglMapboxProvider = ({ children }: PropsWithChildren) => {

const addLayer = useCallback(
(layer: Layer) => {
layersRef.current = [...layersRef.current, layer];
deckGLMapboxOverlay.setProps({ layers: layersRef.current });
// Artificially delay adding the layer to give a chance to React Map Gl to render the
// positioning layers first
setTimeout(() => {
layersRef.current = [...layersRef.current, layer];
deckGLMapboxOverlay.setProps({ layers: layersRef.current });
}, 0);
},
[deckGLMapboxOverlay],
);

const removeLayer = useCallback(
(layerId: string) => {
layersRef.current = layersRef.current.filter(({ id }) => id !== layerId);
deckGLMapboxOverlay.setProps({ layers: layersRef.current });
// Artificially delay removing the layer to match the `addLayer` function
// Without this, Deck.gl would throw an assertion error
setTimeout(() => {
layersRef.current = layersRef.current.filter(({ id }) => id !== layerId);
deckGLMapboxOverlay.setProps({ layers: layersRef.current });
}, 0);
},
[deckGLMapboxOverlay],
);
Expand Down
7 changes: 6 additions & 1 deletion client/src/components/map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,18 @@ const Map = () => {
// The inner map is memoized so that it doesn't rerender when the map is panned due to the bounds
// changing
const innerMap = useMemo(() => {
// We make sure to render the layers when the map is ready
if (!map) {
return null;
}

return (
<DeckglMapboxProvider>
<LayerManager />
<Controls />
</DeckglMapboxProvider>
);
}, []);
}, [map]);

const onMove = useCallback(() => {
setBounds(map?.getBounds()?.toArray() as [LngLatLike, LngLatLike]);
Expand Down
11 changes: 6 additions & 5 deletions client/src/components/map/layer-manager/animated-layer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MaskExtension } from "@deck.gl/extensions";
import { TileLayer } from "@deck.gl/geo-layers";
import { BitmapLayer } from "@deck.gl/layers";
import { GL } from "@luma.gl/constants";
import parseAPNG from "apng-js";
import { getMonth } from "date-fns";
import { useContext, useEffect } from "react";
Expand Down Expand Up @@ -79,12 +79,13 @@ const AnimatedLayer = ({ config, date, beforeId }: AnimatedLayerProps) => {
visible: subLayer.visible,
opacity: subLayer.opacity,
textureParameters: {
[GL.TEXTURE_MIN_FILTER]: GL.NEAREST,
[GL.TEXTURE_MAG_FILTER]: GL.NEAREST,
[GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE,
[GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE,
minFilter: "nearest",
magFilter: "nearest",
mipmapFilter: undefined,
},
image: subLayer.data[frameIndex].bitmapData,
extensions: [new MaskExtension()],
maskId: "mask",
});
},
});
Expand Down
51 changes: 32 additions & 19 deletions client/src/components/map/layer-manager/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMemo } from "react";
import { Layer } from "react-map-gl";

import MaskLayer from "@/components/map/layer-manager/mask-layer";
import useMapLayers from "@/hooks/use-map-layers";

import LayerManagerItem from "./item";
Expand All @@ -13,28 +14,40 @@ const LayerManager = () => {
* See more: https://github.com/visgl/react-map-gl/issues/939#issuecomment-625290200
*/
const positioningLayers = useMemo(() => {
return layers.map((layer, index) => {
const beforeId = index === 0 ? "data-layers" : `layer-position-${layers[index - 1].id}`;
return (
<Layer
key={`layer-position-${layer.id}`}
id={`layer-position-${layer.id}`}
type="background"
layout={{ visibility: "none" }}
beforeId={beforeId}
/>
);
});
return [
...layers.map((layer, index) => {
const beforeId = index === 0 ? "data-layers" : `layer-position-${layers[index - 1].id}`;
return (
<Layer
key={`layer-position-${layer.id}`}
id={`layer-position-${layer.id}`}
type="background"
layout={{ visibility: "none" }}
beforeId={beforeId}
/>
);
}),
<Layer
key="layer-position-mask"
id="layer-position-mask"
type="background"
layout={{ visibility: "none" }}
beforeId={layers.length === 0 ? "data-layers" : `layer-position-${layers.slice(-1)[0].id}`}
/>,
];
}, [layers]);

const layerManagerItems = useMemo(() => {
return layers.map((layer, index) => {
const beforeId = index === 0 ? "data-layers" : `layer-position-${layers[index - 1].id}`;
const { id, ...settings } = layer;
return (
<LayerManagerItem key={`layer-${id}`} id={id} settings={settings} beforeId={beforeId} />
);
});
return [
...layers.map((layer) => {
const beforeId = `layer-position-${layer.id}`;
const { id, ...settings } = layer;
return (
<LayerManagerItem key={`layer-${id}`} id={id} settings={settings} beforeId={beforeId} />
);
}),
<MaskLayer key="layer-mask" beforeId="layer-position-mask" />,
];
}, [layers]);

return (
Expand Down
14 changes: 12 additions & 2 deletions client/src/components/map/layer-manager/item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import useLayerConfig from "@/hooks/use-layer-config";
import { LayerSettings } from "@/types/layer";

import AnimatedLayer from "./animated-layer";
import StaticLayer from "./static-layer";
import RasterLayer from "./raster-layer";
import VectorLayer from "./vector-layer";

interface LayerManagerItemProps {
id: number;
Expand All @@ -27,7 +28,16 @@ const LayerManagerItem = ({ id, beforeId, settings }: LayerManagerItemProps) =>
return <AnimatedLayer config={config} date={settings.date} beforeId={beforeId} />;
}

return <StaticLayer config={config} beforeId={beforeId} />;
if (config.source.type === "raster") {
return <RasterLayer config={config} beforeId={beforeId} />;
}

if (config.source.type === "vector") {
return <VectorLayer config={config} beforeId={beforeId} />;
}

console.warn(`Unsupported layer type (${config.source.type})`);
return null;
};

export default LayerManagerItem;
52 changes: 52 additions & 0 deletions client/src/components/map/layer-manager/mask-layer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { GeoJsonLayer } from "@deck.gl/layers";
import { useContext, useEffect, useMemo } from "react";

import useLocation from "@/hooks/use-location";
import { useLocationGeometry } from "@/hooks/use-location-geometry";

import { DeckGLMapboxOverlayContext } from "../deckgl-mapbox-provider";

interface MaskLayerProps {
beforeId: string;
}

const MaskLayer = ({ beforeId }: MaskLayerProps) => {
const [location] = useLocation();
const { data, isLoading } = useLocationGeometry(location.code.slice(-1)[0]);
const geometry = useMemo(() => {
if (isLoading || data === undefined || data === null) {
// We return an empty feature collection so that while the geometry is loading, we don't show
// anything instead of the layers unmasked
return {
type: "FeatureCollection",
features: [],
};
}

return data;
}, [data, isLoading]);

const { addLayer, removeLayer } = useContext(DeckGLMapboxOverlayContext);

useEffect(() => {
const layer = new GeoJsonLayer({
id: "mask",
beforeId,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
data: geometry,
stroked: false,
operation: "mask",
});

addLayer(layer);

return () => {
removeLayer("mask");
};
}, [addLayer, beforeId, geometry, removeLayer]);

return null;
};

export default MaskLayer;
69 changes: 69 additions & 0 deletions client/src/components/map/layer-manager/raster-layer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { MaskExtension } from "@deck.gl/extensions";
import { TileLayer } from "@deck.gl/geo-layers";
import { BitmapLayer } from "@deck.gl/layers";
import { useContext, useEffect } from "react";
import { RasterLayer as IRasterLayer, RasterSource as IRasterSource } from "react-map-gl";

import { LayerConfig } from "@/types/layer";

import { DeckGLMapboxOverlayContext } from "../deckgl-mapbox-provider";

interface RasterLayerProps {
config: LayerConfig;
beforeId: string;
}

const RasterLayer = ({ config, beforeId }: RasterLayerProps) => {
const { addLayer, removeLayer } = useContext(DeckGLMapboxOverlayContext);

useEffect(() => {
const style = config.styles[0] as IRasterLayer;
const source = config.source as IRasterSource;

const layer = new TileLayer({
id: style.id,
beforeId,
data: source.tiles,
tileSize: source.tileSize,
minZoom: source.minzoom,
maxZoom: source.maxzoom,
visible: style.layout?.visibility !== "none",
opacity: style.paint?.["raster-opacity"] as number,
renderSubLayers: (subLayer) => {
if (!subLayer || !subLayer.data || !subLayer.tile) {
return null;
}

return new BitmapLayer({
id: subLayer.id,
bounds: [
subLayer.tile.boundingBox[0][0],
subLayer.tile.boundingBox[0][1],
subLayer.tile.boundingBox[1][0],
subLayer.tile.boundingBox[1][1],
],
visible: subLayer.visible,
opacity: subLayer.opacity,
textureParameters: {
minFilter: "nearest",
magFilter: "nearest",
mipmapFilter: undefined,
},
image: subLayer.data,
extensions: [new MaskExtension()],
maskId: "mask",
});
},
});

addLayer(layer);

return () => {
removeLayer(config.styles[0].id);
};
}, [config, beforeId, addLayer, removeLayer]);

return null;
};

export default RasterLayer;
25 changes: 0 additions & 25 deletions client/src/components/map/layer-manager/static-layer.tsx

This file was deleted.

Loading

0 comments on commit a207052

Please sign in to comment.