Skip to content

Commit

Permalink
feat: support MVT on reearth/core (#388)
Browse files Browse the repository at this point in the history
* chore: remove legacy prefix for Resource component

* feat: support CZML

* feat: add extractSimpleLayerData util

* Update src/core/engines/Cesium/Feature/Resource/index.stories.tsx

Co-authored-by: rot1024 <aayhrot@gmail.com>

* feat: support raster appearance and wms

* chore: rename

* fix: data type

* test: fix storybook data

* fix: remove zindex

* fix: restrict pickProperty

* feat: support MVT on reearth/core

* feat: upgrade cesium-mvt-imagery-provider

* fix: improve eval feature

* fix: delegated data process

* chore: fix versioning

* fix: simplify condition

* fix: cyclic import

* fix: use display config

* refactor: evalFeature

* fix: use Map for ComputedFeature

* fix: unuse onFeatureFetch

* Update src/core/Map/Layer/index.tsx

Co-authored-by: rot1024 <aayhrot@gmail.com>

* fix: merge error

* fix: import error

Co-authored-by: rot1024 <aayhrot@gmail.com>
  • Loading branch information
keiya01 and rot1024 committed Jan 10, 2023
1 parent 7280af5 commit cac89cc
Show file tree
Hide file tree
Showing 18 changed files with 362 additions and 67 deletions.
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -55,6 +55,7 @@
"@types/gapi.client.sheets": "4.0.20201030",
"@types/google.picker": "0.0.39",
"@types/lodash-es": "4.17.6",
"@types/mapbox__vector-tile": "1.3.0",
"@types/node": "18.11.18",
"@types/react": "18.0.26",
"@types/react-dom": "18.0.10",
Expand Down Expand Up @@ -96,6 +97,7 @@
"@auth0/auth0-react": "1.12.0",
"@emotion/react": "11.10.5",
"@emotion/styled": "11.10.5",
"@mapbox/vector-tile": "1.3.1",
"@floating-ui/react-dom": "1.1.2",
"@monaco-editor/react": "4.4.6",
"@popperjs/core": "2.11.6",
Expand All @@ -110,6 +112,7 @@
"axios": "1.2.2",
"cesium": "1.101.0",
"cesium-dnd": "1.1.0",
"cesium-mvt-imagery-provider": "1.1.0",
"core-js": "3.27.1",
"crypto-js": "4.1.1",
"csv-parse": "5.3.3",
Expand Down
3 changes: 2 additions & 1 deletion src/core/Map/Layer/hooks.ts
@@ -1,7 +1,7 @@
import { useAtom } from "jotai";
import { useCallback, useLayoutEffect, useMemo } from "react";

import { computeAtom, DataType, type Atom } from "../../mantle";
import { computeAtom, DataType, type Atom, evalFeature } from "../../mantle";
import type { DataRange, Feature, Layer } from "../../mantle";

export type { Atom as Atoms } from "../../mantle";
Expand Down Expand Up @@ -54,5 +54,6 @@ export default function useHooks(
handleFeatureRequest: requestFetch,
handleFeatureFetch: writeFeatures,
handleFeatureDelete: deleteFeatures,
evalFeature,
};
}
25 changes: 18 additions & 7 deletions src/core/Map/Layer/index.tsx
@@ -1,6 +1,14 @@
import { ComponentType } from "react";

import type { DataRange, Feature, ComputedLayer, Layer, DataType } from "../../mantle";
import type {
DataRange,
Feature,
ComputedLayer,
Layer,
DataType,
LayerSimple,
ComputedFeature,
} from "../../mantle";

import useHooks, { type Atoms } from "./hooks";

Expand All @@ -21,6 +29,7 @@ export type FeatureComponentProps = {
onFeatureRequest?: (range: DataRange) => void;
onFeatureFetch?: (features: Feature[]) => void;
onFeatureDelete?: (features: string[]) => void;
evalFeature?: (layer: LayerSimple, feature: Feature) => ComputedFeature | undefined;
} & CommonProps;

export type Props = {
Expand All @@ -40,19 +49,21 @@ export default function LayerComponent({
delegatedDataTypes,
...props
}: Props): JSX.Element | null {
const { computedLayer, handleFeatureDelete, handleFeatureFetch, handleFeatureRequest } = useHooks(
Feature ? layer : undefined,
atoms,
overrides,
delegatedDataTypes,
);
const {
computedLayer,
handleFeatureDelete,
handleFeatureFetch,
handleFeatureRequest,
evalFeature,
} = useHooks(Feature ? layer : undefined, atoms, overrides, delegatedDataTypes);

return layer && computedLayer && Feature ? (
<Feature
layer={computedLayer}
onFeatureDelete={handleFeatureDelete}
onFeatureFetch={handleFeatureFetch}
onFeatureRequest={handleFeatureRequest}
evalFeature={evalFeature}
{...props}
/>
) : null;
Expand Down
2 changes: 2 additions & 0 deletions src/core/Map/Layers/index.test.tsx
Expand Up @@ -29,6 +29,7 @@ test("simple", () => {
onFeatureDelete: expect.any(Function),
onFeatureFetch: expect.any(Function),
onFeatureRequest: expect.any(Function),
evalFeature: expect.any(Function),
});
expect(Feature.mock.calls[1][0]).toEqual({
isHidden: false,
Expand All @@ -43,6 +44,7 @@ test("simple", () => {
onFeatureDelete: expect.any(Function),
onFeatureFetch: expect.any(Function),
onFeatureRequest: expect.any(Function),
evalFeature: expect.any(Function),
});

Feature.mockClear();
Expand Down
159 changes: 159 additions & 0 deletions src/core/engines/Cesium/Feature/Raster/hooks.ts
@@ -0,0 +1,159 @@
import { VectorTileFeature } from "@mapbox/vector-tile";
import { ImageryLayerCollection, ImageryProvider, WebMapServiceImageryProvider } from "cesium";
import { MVTImageryProvider } from "cesium-mvt-imagery-provider";
import { useEffect, useMemo, useRef, useState } from "react";
import { useCesium } from "resium";

import { ComputedFeature, ComputedLayer, Feature } from "@reearth/core/mantle";

import { extractSimpleLayerData } from "../utils";

import { Props } from "./types";

const useImageryProvider = (imageryProvider: ImageryProvider | undefined) => {
const { viewer } = useCesium();
useEffect(() => {
if (!imageryProvider) return;
const imageryLayers: ImageryLayerCollection = viewer.imageryLayers;
const layer = imageryLayers.addImageryProvider(imageryProvider);
return () => {
imageryLayers.remove(layer);
};
}, [imageryProvider, viewer]);
};

const useData = (layer: ComputedLayer | undefined) => {
return useMemo(() => {
const data = extractSimpleLayerData(layer);
return {
type: data?.type,
url: data?.url,
layers: data?.layers
? Array.isArray(data.layers)
? data.layers.join(",")
: data?.layers
: undefined,
};
}, [layer]);
};

export const useWMS = ({
isVisible,
property,
layer,
}: Pick<Props, "isVisible" | "property" | "layer">) => {
const { minimumLevel, maximumLevel, credit } = property ?? {};
const { type, url, layers } = useData(layer);

const imageryProvider = useMemo(() => {
if (!isVisible || !url || !layers || type !== "wms") return;
return new WebMapServiceImageryProvider({
url,
layers,
minimumLevel,
maximumLevel,
credit,
});
}, [isVisible, type, url, minimumLevel, maximumLevel, credit, layers]);

useImageryProvider(imageryProvider);
};

type TileCoords = { x: number; y: number; level: number };

const idFromGeometry = (tile: TileCoords) => [tile.x, tile.y, tile.level].join(":");

const makeFeatureFromPolygon = (
id: string,
feature: VectorTileFeature,
tile: TileCoords,
): Feature => {
const geometry = feature.loadGeometry();
const coordinates = geometry.map(points => points.map(p => [p.x, p.y]));
return {
id,
geometry: {
type: "Polygon",
coordinates,
},
properties: feature.properties,
range: {
x: tile.x,
y: tile.y,
z: tile.level,
},
};
};

export const useMVT = ({
isVisible,
property,
layer,
evalFeature,
}: Pick<Props, "isVisible" | "property" | "layer" | "onFeatureFetch" | "evalFeature">) => {
const { minimumLevel, maximumLevel, credit } = property ?? {};
const { type, url, layers } = useData(layer);

const [cachedFeatureIds] = useState(() => new Set<Feature["id"]>());
const cachedFeaturesRef = useRef<Feature[]>([]);
const cachedComputedFeaturesRef = useRef<Map<Feature["id"], ComputedFeature>>(new Map());
const cachedCalculatedLayerRef = useRef(layer);

const imageryProvider = useMemo(() => {
if (!isVisible || !url || !layers || type !== "mvt") return;
return new MVTImageryProvider({
minimumLevel,
maximumLevel,
credit,
urlTemplate: url as `http${"s" | ""}://${string}/{z}/{x}/{y}${string}`,
layerName: layers,
style: (mvtFeature, tile) => {
const id = mvtFeature.id ? String(mvtFeature.id) : idFromGeometry(tile);
const feature = ((): ComputedFeature | void => {
if (!cachedFeatureIds.has(id)) {
const layer = cachedCalculatedLayerRef.current?.layer;
if (
layer?.type === "simple" &&
VectorTileFeature.types[mvtFeature.type] === "Polygon"
) {
const feature = makeFeatureFromPolygon(id, mvtFeature, tile);
cachedFeatureIds.add(id);
cachedFeaturesRef.current.push(feature);
const computedFeature = evalFeature?.(layer, feature);
if (computedFeature) {
cachedComputedFeaturesRef.current.set(id, computedFeature);
}
return computedFeature;
}
} else {
return cachedComputedFeaturesRef.current.get(
mvtFeature.id ? String(mvtFeature.id) : idFromGeometry(tile),
);
}
})();
return {
fillStyle: feature?.polygon?.fillColor,
strokeStyle: feature?.polygon?.strokeColor,
lineWidth: feature?.polygon?.strokeWidth,
lineJoin: feature?.polygon?.lineJoin,
};
},
});
}, [
isVisible,
type,
url,
minimumLevel,
maximumLevel,
credit,
layers,
cachedFeatureIds,
evalFeature,
]);

useEffect(() => {
cachedCalculatedLayerRef.current = layer;
}, [layer]);

useImageryProvider(imageryProvider);
};
41 changes: 39 additions & 2 deletions src/core/engines/Cesium/Feature/Raster/index.stories.tsx
Expand Up @@ -11,8 +11,8 @@ export default {

const Template: Story<Props> = args => <Component {...args} />;

export const Default = Template.bind([]);
Default.args = {
export const WMS = Template.bind([]);
WMS.args = {
engine: "cesium",
engines: {
cesium: engine,
Expand All @@ -27,6 +27,43 @@ Default.args = {
url: "https://gibs.earthdata.nasa.gov/wms/epsg3857/best/wms.cgi",
layers: "IMERG_Precipitation_Rate",
},
raster: {
maximumLevel: 100,
},
},
],
property: {
tiles: [
{
id: "default",
tile_type: "default",
},
],
},
};

export const MVT = Template.bind([]);
MVT.args = {
engine: "cesium",
engines: {
cesium: engine,
},
ready: true,
layers: [
{
id: "l",
type: "simple",
data: {
type: "mvt",
url: "https://d2jfi34fqvxlsc.cloudfront.net/main/data/mvt/tran/13100_tokyo/{z}/{x}/{y}.mvt",
layers: "road",
},
polygon: {
fillColor: "white",
strokeColor: "white",
strokeWidth: 1,
lineJoin: "round",
},
raster: {},
},
],
Expand Down
51 changes: 6 additions & 45 deletions src/core/engines/Cesium/Feature/Raster/index.tsx
@@ -1,50 +1,11 @@
import { ImageryLayerCollection, WebMapServiceImageryProvider } from "cesium";
import { useEffect, useMemo } from "react";
import { useCesium } from "resium";
import { type FeatureComponentConfig } from "../utils";

import type { RasterAppearance } from "../../..";
import { extractSimpleLayerData, type FeatureComponentConfig, type FeatureProps } from "../utils";
import { useMVT, useWMS } from "./hooks";
import type { Props } from "./types";

export type Props = FeatureProps<Property>;
export type Property = RasterAppearance;

const ImageryProviders = {
wms: WebMapServiceImageryProvider,
};

export default function Raster({ isVisible, layer, property }: Props) {
const { minimumLevel, maximumLevel, credit } = property ?? {};
const { viewer } = useCesium();
const { type, url, layers } = useMemo(() => {
const data = extractSimpleLayerData(layer);
return {
type: data?.type as keyof typeof ImageryProviders,
url: data?.url,
layers: data?.layers
? Array.isArray(data.layers)
? data.layers.join(",")
: data?.layers
: undefined,
};
}, [layer]);

const imageryProvider = type ? ImageryProviders[type] : undefined;

useEffect(() => {
if (!isVisible || !imageryProvider || !url || !layers) return;
const provider = new imageryProvider({
url,
layers,
minimumLevel,
maximumLevel,
credit,
});
const imageryLayers: ImageryLayerCollection = viewer.imageryLayers;
const layer = imageryLayers.addImageryProvider(provider);
return () => {
imageryLayers.remove(layer);
};
}, [isVisible, imageryProvider, url, viewer, layers, minimumLevel, maximumLevel, credit]);
export default function Raster({ isVisible, layer, property, evalFeature }: Props) {
useWMS({ isVisible, layer, property });
useMVT({ isVisible, layer, property, evalFeature });

return null;
}
Expand Down
6 changes: 6 additions & 0 deletions src/core/engines/Cesium/Feature/Raster/types.ts
@@ -0,0 +1,6 @@
import { RasterAppearance } from "@reearth/core/mantle";

import { FeatureProps } from "../utils";

export type Props = FeatureProps<Property>;
export type Property = RasterAppearance;

0 comments on commit cac89cc

Please sign in to comment.