diff --git a/src/core/engines/Cesium/Feature/Box/index.tsx b/src/core/engines/Cesium/Feature/Box/index.tsx index d3025dbc0..e1896834a 100644 --- a/src/core/engines/Cesium/Feature/Box/index.tsx +++ b/src/core/engines/Cesium/Feature/Box/index.tsx @@ -26,6 +26,7 @@ const Box: React.FC = memo(function BoxPresenter({ onLayerEdit, }) { const { + show = true, height = 100, width = 100, length = 100, @@ -52,10 +53,9 @@ const Box: React.FC = memo(function BoxPresenter({ } = useHooks({ property, geometry, feature, onLayerEdit }); const scalePointDimension = ((width + height + length) / 3) * 0.05; - const [layerId, featureId] = [layer?.id, feature?.id]; - return !isVisible ? null : ( + return !isVisible || !show ? null : ( <> {SIDE_PLANES.map((plane, i) => ( geometry?.type === "Point" @@ -58,7 +59,7 @@ export default function Ellipsoid({ id, isVisible, property, geometry, layer, fe [property?.near, property?.far], ); - return !isVisible || !pos ? null : ( + return !isVisible || !pos || !show ? null : ( {extrudePoints && ( geometry?.type === "LineString" @@ -47,7 +48,7 @@ export default function Polyline({ id, isVisible, property, geometry, layer, fea [property?.near, property?.far], ); - return !isVisible || !coordinates ? null : ( + return !isVisible || !coordinates || !show ? null : ( { + const f = ["a", "b"] as const; + const p: { a?: number; b?: number; c?: number } = { a: 1, b: 2, c: 3 }; + const { result, rerender } = renderHook(({ p }) => usePick(p, f), { initialProps: { p: p } }); + + expect(result.current).toEqual({ a: 1, b: 2 }); + const first = result.current; + + rerender({ p: { a: p.a, b: p.b, c: p.c } }); + expect(result.current).toBe(first); + expect(result.current).toEqual({ a: 1, b: 2 }); + + rerender({ p: { c: p.c, b: p.b, a: p.a } }); + expect(result.current).toBe(first); + expect(result.current).toEqual({ a: 1, b: 2 }); + + rerender({ p: { b: p.b, a: p.a } }); + expect(result.current).toBe(first); + expect(result.current).toEqual({ a: 1, b: 2 }); + + rerender({ p: { a: 10 } }); + expect(result.current).not.toBe(first); + expect(result.current).toEqual({ a: 10 }); +}); diff --git a/src/core/engines/Cesium/Feature/Raster/hooks.ts b/src/core/engines/Cesium/Feature/Raster/hooks.ts index 9f13e7fc4..3df20661e 100644 --- a/src/core/engines/Cesium/Feature/Raster/hooks.ts +++ b/src/core/engines/Cesium/Feature/Raster/hooks.ts @@ -7,10 +7,11 @@ import { } from "cesium"; import { MVTImageryProvider } from "cesium-mvt-imagery-provider"; import md5 from "js-md5"; +import { isEqual, pick } from "lodash-es"; import { useEffect, useMemo, useRef } from "react"; import { useCesium } from "resium"; -import type { ComputedFeature, ComputedLayer, Feature } from "../../.."; +import type { ComputedFeature, ComputedLayer, Feature, PolygonAppearance } from "../../.."; import { extractSimpleLayer, extractSimpleLayerData } from "../utils"; import { Props } from "./types"; @@ -47,11 +48,11 @@ export const useWMS = ({ property, layer, }: Pick) => { - const { minimumLevel, maximumLevel, credit } = property ?? {}; + const { show = true, minimumLevel, maximumLevel, credit } = property ?? {}; const { type, url, layers } = useData(layer); const imageryProvider = useMemo(() => { - if (!isVisible || !url || !layers || type !== "wms") return; + if (!isVisible || !show || !url || !layers || type !== "wms") return; return new WebMapServiceImageryProvider({ url, layers, @@ -59,7 +60,7 @@ export const useWMS = ({ maximumLevel, credit, }); - }, [isVisible, type, url, minimumLevel, maximumLevel, credit, layers]); + }, [isVisible, show, url, layers, type, minimumLevel, maximumLevel, credit]); useImageryProvider(imageryProvider); }; @@ -114,7 +115,7 @@ export const useMVT = ({ Props, "isVisible" | "property" | "layer" | "onComputedFeatureFetch" | "evalFeature" | "onFeatureDelete" >) => { - const { minimumLevel, maximumLevel, credit } = property ?? {}; + const { show = true, minimumLevel, maximumLevel, credit } = property ?? {}; const { type, url, layers } = useData(layer); const cachedFeaturesRef = useRef>(new Map()); @@ -125,15 +126,12 @@ export const useMVT = ({ const shouldSyncFeatureRef = useRef(false); const layerSimple = extractSimpleLayer(layer); - const polygonAppearanceFillStyle = layerSimple?.polygon?.fillColor; - const polygonAppearanceStrokeStyle = layerSimple?.polygon?.strokeColor; - const polygonAppearanceLineWidth = layerSimple?.polygon?.strokeWidth; - const polygonAppearanceLineJoin = layerSimple?.polygon?.lineJoin; + const layerPolygonAppearance = usePick(layerSimple?.polygon, polygonAppearanceFields); const tempFeaturesRef = useRef([]); const tempComputedFeaturesRef = useRef([]); const imageryProvider = useMemo(() => { - if (!isVisible || !url || !layers || type !== "mvt") return; + if (!isVisible || !show || !url || !layers || type !== "mvt") return; return new MVTImageryProvider({ minimumLevel, maximumLevel, @@ -183,16 +181,11 @@ export const useMVT = ({ return; } - if ( - polygonAppearanceFillStyle !== feature?.properties.fillColor || - polygonAppearanceStrokeStyle !== feature?.properties.strokeStyle || - polygonAppearanceLineWidth !== feature?.properties.lineWidth || - polygonAppearanceLineJoin !== feature?.properties.lineJoin - ) { - feature.properties.fillColor = polygonAppearanceFillStyle; - feature.properties.strokeStyle = polygonAppearanceStrokeStyle; - feature.properties.lineWidth = polygonAppearanceLineWidth; - feature.properties.lineJoin = polygonAppearanceLineJoin; + const featurePolygonAppearance = pick(feature?.properties, polygonAppearanceFields); + if (!isEqual(layerPolygonAppearance, featurePolygonAppearance)) { + Object.entries(layerPolygonAppearance ?? {}).forEach(([k, v]) => { + feature.properties[k] = v; + }); const computedFeature = evalFeature?.(layer, feature); if (computedFeature) { @@ -214,11 +207,16 @@ export const useMVT = ({ tempComputedFeaturesRef.current.push(computedFeature); } + const polygon = computedFeature?.polygon; return { - fillStyle: computedFeature?.polygon?.fillColor, - strokeStyle: computedFeature?.polygon?.strokeColor, - lineWidth: computedFeature?.polygon?.strokeWidth, - lineJoin: computedFeature?.polygon?.lineJoin, + fillStyle: + (polygon?.fill ?? true) && (polygon?.show ?? true) + ? polygon?.fillColor + : "rgba(0,0,0,0)", // hide the feature + strokeStyle: + polygon?.stroke && (polygon?.show ?? true) ? polygon?.strokeColor : "rgba(0,0,0,0)", // hide the feature + lineWidth: polygon?.strokeWidth, + lineJoin: polygon?.lineJoin, }; }, onSelectFeature: (mvtFeature, tile) => { @@ -234,18 +232,16 @@ export const useMVT = ({ }); }, [ isVisible, - type, + show, url, + layers, + type, minimumLevel, maximumLevel, credit, - layers, - evalFeature, onComputedFeatureFetch, - polygonAppearanceFillStyle, - polygonAppearanceStrokeStyle, - polygonAppearanceLineWidth, - polygonAppearanceLineJoin, + evalFeature, + layerPolygonAppearance, ]); useEffect(() => { @@ -261,3 +257,22 @@ export const useMVT = ({ useImageryProvider(imageryProvider); }; + +export const usePick = ( + o: T | undefined | null, + fields: readonly U[], +): Pick | undefined => { + const p = useMemo(() => (o ? pick(o, fields) : undefined), [o, fields]); + // eslint-disable-next-line react-hooks/exhaustive-deps + return useMemo(() => p, [JSON.stringify(p)]); +}; + +const polygonAppearanceFields: (keyof PolygonAppearance)[] = [ + "show", + "fill", + "fillColor", + "stroke", + "strokeColor", + "strokeWidth", + "lineJoin", +]; diff --git a/src/core/engines/Cesium/Feature/Resource/index.tsx b/src/core/engines/Cesium/Feature/Resource/index.tsx index 47a88f9d3..ea5d59bfa 100644 --- a/src/core/engines/Cesium/Feature/Resource/index.tsx +++ b/src/core/engines/Cesium/Feature/Resource/index.tsx @@ -42,7 +42,7 @@ type CachedFeature = { }; export default function Resource({ isVisible, property, layer, onComputedFeatureFetch }: Props) { - const { clampToGround } = property ?? {}; + const { show = true, clampToGround } = property ?? {}; const [type, url] = useMemo((): [ResourceAppearance["type"], string | undefined] => { const data = extractSimpleLayerData(layer); const type = property?.type; @@ -92,7 +92,7 @@ export default function Resource({ isVisible, property, layer, onComputedFeature }); }, [layer, viewer]); - if (!isVisible || !Component || !url) return null; + if (!isVisible || !show || !Component || !url) return null; return ( diff --git a/src/core/engines/Cesium/Feature/Resource/utils.ts b/src/core/engines/Cesium/Feature/Resource/utils.ts index b23b0232b..71cc5036c 100644 --- a/src/core/engines/Cesium/Feature/Resource/utils.ts +++ b/src/core/engines/Cesium/Feature/Resource/utils.ts @@ -29,7 +29,7 @@ type CesiumEntityAppearanceKey = "polygon" | "polyline"; type SupportedAppearanceKey = "marker" | keyof Pick; type EntityAppearanceKey = AName extends "marker" - ? keyof Pick + ? keyof Pick : keyof Pick; type AppearancePropertyKeyType = "color" | "heightReference" | "shadows"; @@ -107,7 +107,12 @@ export const attachStyle = ( if (!layer) { return; } - if (hasAppearance(layer, entity, ["marker", "point"])) { + + // TODO: make it DRY + const point = hasAppearance(layer, entity, ["marker", "point"]); + const billboard = hasAppearance(layer, entity, ["marker", "billboard"]); + const label = hasAppearance(layer, entity, ["marker", "label"]); + if (point || billboard || label) { const position = entity.position?.getValue(currentTime); const coordinates = [position?.x ?? 0, position?.y ?? 0, position?.z ?? 0]; const feature: Feature = { @@ -128,26 +133,81 @@ export const attachStyle = ( if (!computedFeature) { return; } - attachProperties(entity, computedFeature, ["marker", "point"], { - pixelSize: { - name: "pointSize", - }, - color: { - name: "pointColor", - type: "color", - }, - outlineColor: { - name: "pointOutlineColor", - type: "color", - }, - outlineWidth: { - name: "pointOutlineWidth", - }, - heightReference: { - name: "heightReference", - type: "heightReference", - }, - }); + if (point) { + attachProperties(entity, computedFeature, ["marker", "point"], { + show: { + name: "show", + }, + pixelSize: { + name: "pointSize", + }, + color: { + name: "pointColor", + type: "color", + }, + outlineColor: { + name: "pointOutlineColor", + type: "color", + }, + outlineWidth: { + name: "pointOutlineWidth", + }, + heightReference: { + name: "heightReference", + type: "heightReference", + }, + }); + } + + if (billboard) { + attachProperties(entity, computedFeature, ["marker", "billboard"], { + show: { + name: "show", + }, + image: { + name: "image", + }, + color: { + name: "imageColor", + type: "color", + }, + scale: { + name: "imageSize", + }, + sizeInMeters: { + name: "imageSizeInMeters", + }, + heightReference: { + name: "heightReference", + type: "heightReference", + }, + horizontalOrigin: { + name: "imageHorizontalOrigin", + }, + verticalOrigin: { + name: "imageVerticalOrigin", + }, + }); + + if (label) { + attachProperties(entity, computedFeature, ["marker", "label"], { + show: { + name: "show", + }, + text: { + name: "labelText", + }, + backgroundColor: { + name: "labelBackground", + type: "color", + }, + heightReference: { + name: "heightReference", + type: "heightReference", + }, + }); + } + } return [feature, computedFeature]; } @@ -178,6 +238,9 @@ export const attachStyle = ( return; } attachProperties(entity, computedFeature, ["polyline", "polyline"], { + show: { + name: "show", + }, width: { name: "strokeWidth", }, @@ -221,6 +284,9 @@ export const attachStyle = ( return; } attachProperties(entity, computedFeature, ["polygon", "polygon"], { + show: { + name: "show", + }, fill: { name: "fill", }, diff --git a/src/core/engines/Cesium/Feature/Tileset/hooks.ts b/src/core/engines/Cesium/Feature/Tileset/hooks.ts index 076e7867f..039ac5dbe 100644 --- a/src/core/engines/Cesium/Feature/Tileset/hooks.ts +++ b/src/core/engines/Cesium/Feature/Tileset/hooks.ts @@ -96,11 +96,14 @@ const useFeature = ({ const layer = cachedCalculatedLayerRef?.current?.layer; if (layer?.type === "simple" && feature?.feature) { const computedFeature = evalFeature(layer, feature?.feature); - const show = computedFeature?.["3dtiles"]?.show; + const style = computedFeature?.["3dtiles"]; + + const show = style?.show; if (show !== undefined) { feature.raw.show = show; } - const color = toColor(computedFeature?.["3dtiles"]?.color); + + const color = toColor(style?.color); if (color !== undefined) { feature.raw.color = color; } diff --git a/src/core/mantle/types/appearance.ts b/src/core/mantle/types/appearance.ts index b0f9bf873..55ef473cc 100644 --- a/src/core/mantle/types/appearance.ts +++ b/src/core/mantle/types/appearance.ts @@ -25,6 +25,7 @@ export type AppearanceTypes = { }; export type MarkerAppearance = { + show?: boolean; heightReference?: "none" | "clamp" | "relative"; style?: "none" | "point" | "image"; pointSize?: number; @@ -65,6 +66,7 @@ export type MarkerAppearance = { }; export type PolylineAppearance = { + show?: boolean; clampToGround?: boolean; strokeColor?: string; strokeWidth?: number; @@ -74,6 +76,7 @@ export type PolylineAppearance = { }; export type PolygonAppearance = { + show?: boolean; fill?: boolean; fillColor?: string; stroke?: boolean; @@ -88,6 +91,7 @@ export type PolygonAppearance = { }; export type EllipsoidAppearance = { + show?: boolean; heightReference?: "none" | "clamp" | "relative"; shadows?: "disabled" | "enabled" | "cast_only" | "receive_only"; radius?: number; @@ -97,6 +101,7 @@ export type EllipsoidAppearance = { }; export type ModelAppearance = { + show?: boolean; model?: string; // For compat url?: string; heightReference?: "none" | "clamp" | "relative"; @@ -121,7 +126,6 @@ export type ModelAppearance = { }; export type Cesium3DTilesAppearance = { - tileset?: string; show?: boolean; color?: string; styleUrl?: string; @@ -129,10 +133,12 @@ export type Cesium3DTilesAppearance = { colorBlendMode?: "highlight" | "replace" | "mix" | "default"; edgeWidth?: number; edgeColor?: string; + tileset?: string; experimental_clipping?: EXPERIMENTAL_clipping; }; export type LegacyPhotooverlayAppearance = { + show?: boolean; location?: LatLng; height?: number; heightReference?: "none" | "clamp" | "relative"; @@ -154,18 +160,21 @@ export type LegacyPhotooverlayAppearance = { }; export type ResourceAppearance = { + show?: boolean; url?: string; type?: "geojson" | "kml" | "czml" | "auto"; clampToGround?: boolean; }; export type RasterAppearance = { + show?: boolean; minimumLevel?: number; maximumLevel?: number; credit?: string; }; export type BoxAppearance = { + show?: boolean; height?: number; width?: number; length?: number;