diff --git a/src/core/engines/Cesium/Feature/Box/Edge.tsx b/src/core/engines/Cesium/Feature/Box/Edge.tsx index 7f6da9e754..f32f288805 100644 --- a/src/core/engines/Cesium/Feature/Box/Edge.tsx +++ b/src/core/engines/Cesium/Feature/Box/Edge.tsx @@ -1,4 +1,10 @@ -import { ArcType, Cartesian3, Color, TranslationRotationScale } from "cesium"; +import { + ArcType, + Cartesian3, + Color, + TimeIntervalCollection, + TranslationRotationScale, +} from "cesium"; import { FC, memo } from "react"; import { PolylineGraphics } from "resium"; @@ -30,6 +36,7 @@ type Props = { fillColor?: Color; hoverColor?: Color; width?: number; + availability?: TimeIntervalCollection; onMouseDown?: EdgeEventCallback; onMouseMove?: EdgeEventCallback; onMouseUp?: EdgeEventCallback; @@ -47,6 +54,7 @@ export const Edge: FC = memo(function EdgePresenter({ trs, width, hoverColor, + availability, onMouseDown, onMouseMove, onMouseUp, @@ -65,7 +73,7 @@ export const Edge: FC = memo(function EdgePresenter({ }); return ( - + = memo(function ScalePointsPresenter({ dimensions, visiblePoint, visibleAxisLine, + availability, onPointMouseDown, onPointMouseMove, onPointMouseUp, @@ -95,7 +103,8 @@ export const ScalePoints: FC = memo(function ScalePointsPresenter({ layerId={`${layerId}-${index}`} featureId={`${featureId}-${index}`} position={entitiesPosition.point} - orientation={orientation}> + orientation={orientation} + availability={availability}> = memo(function ScalePointsPresenter({ layerId={`${layerId}-opposite-${index}`} featureId={`${featureId}-opposite-${index}`} position={entitiesPosition.oppositePoint} - orientation={orientation}> + orientation={orientation} + availability={availability}> = memo(function ScalePointsPresenter({ + featureId={`${featureId}-axis-line-${index}`} + availability={availability}> = memo(function SidePresenter({ layerId, featureId, @@ -26,6 +32,7 @@ export const Side: FC<{ outlineColor, activeOutlineColor, trs, + availability, }) { const { cbRef, plane, dimension, orientation, outlineColorCb } = useHooks({ planeLocal, @@ -36,7 +43,12 @@ export const Side: FC<{ }); return ( - + , "property" | "sceneProperty" | "geometry">) => { + feature, +}: Pick, "property" | "sceneProperty" | "geometry" | "feature">) => { const { viewer } = useCesium(); const ctx = useContext(); const { @@ -291,10 +292,13 @@ export const useHooks = ({ document.body.style.cursor = cursor || "default"; }, [cursor]); + const availability = useMemo(() => toTimeInterval(feature?.interval), [feature?.interval]); + return { style, trs, scalePointStyle, + availability, handlePointMouseDown, handlePointMouseMove, handlePointMouseUp, diff --git a/src/core/engines/Cesium/Feature/Box/index.tsx b/src/core/engines/Cesium/Feature/Box/index.tsx index 991ac1a210..b8e5620204 100644 --- a/src/core/engines/Cesium/Feature/Box/index.tsx +++ b/src/core/engines/Cesium/Feature/Box/index.tsx @@ -40,13 +40,14 @@ const Box: React.FC = memo(function BoxPresenter({ style, trs, scalePointStyle, + availability, handlePointMouseDown, handlePointMouseMove, handlePointMouseUp, handleEdgeMouseDown, handleEdgeMouseMove, handleEdgeMouseUp, - } = useHooks({ property, geometry, sceneProperty }); + } = useHooks({ property, geometry, sceneProperty, feature }); const scalePointDimension = ((width + height + length) / 3) * 0.05; @@ -66,6 +67,7 @@ const Box: React.FC = memo(function BoxPresenter({ isActive={!!activeBox} activeOutlineColor={style.activeOutlineColor} trs={trs} + availability={availability} /> ))} {BOX_EDGES.map((edge, i) => { @@ -86,6 +88,7 @@ const Box: React.FC = memo(function BoxPresenter({ onMouseDown={edge.isDraggable ? handleEdgeMouseDown : undefined} onMouseMove={edge.isDraggable ? handleEdgeMouseMove : undefined} onMouseUp={edge.isDraggable ? handleEdgeMouseUp : undefined} + availability={availability} /> ); })} @@ -115,6 +118,7 @@ const Box: React.FC = memo(function BoxPresenter({ onPointMouseDown={handlePointMouseDown} onPointMouseMove={handlePointMouseMove} onPointMouseUp={handlePointMouseUp} + availability={availability} /> ))} diff --git a/src/core/engines/Cesium/Feature/Ellipsoid/index.tsx b/src/core/engines/Cesium/Feature/Ellipsoid/index.tsx index cf2381b83a..f948fa8dd0 100644 --- a/src/core/engines/Cesium/Feature/Ellipsoid/index.tsx +++ b/src/core/engines/Cesium/Feature/Ellipsoid/index.tsx @@ -7,7 +7,12 @@ import { LatLng, toColor } from "@reearth/util/value"; import type { EllipsoidAppearance } from "../../.."; import { heightReference, shadowMode } from "../../common"; -import { EntityExt, type FeatureComponentConfig, type FeatureProps } from "../utils"; +import { + EntityExt, + toTimeInterval, + type FeatureComponentConfig, + type FeatureProps, +} from "../utils"; export type Props = FeatureProps; @@ -46,6 +51,7 @@ export default function Ellipsoid({ id, isVisible, property, geometry, layer, fe }, [radius]); const material = useMemo(() => toColor(fillColor), [fillColor]); + const availability = useMemo(() => toTimeInterval(feature?.interval), [feature?.interval]); return !isVisible || !pos ? null : ( + legacyLocationPropertyKey="default.position" + availability={availability}> toTimeInterval(feature?.interval), [feature?.interval]); + return !pos || !isVisible ? null : ( <> {extrudePoints && ( - + )} - + {style === "point" ? ( ; @@ -75,6 +80,7 @@ export default function Model({ id, isVisible, property, geometry, layer, featur const modelColor = useMemo(() => (colorBlend ? toColor(color) : undefined), [colorBlend, color]); const modelLightColor = useMemo(() => toColor(lightColor), [lightColor]); const modelSilhouetteColor = useMemo(() => toColor(silhouetteColor), [silhouetteColor]); + const availability = useMemo(() => toTimeInterval(feature?.interval), [feature?.interval]); return !isVisible || (!model && !url) || !position ? null : ( + draggable + availability={availability}> toTimeInterval(feature?.interval), [feature?.interval]); + return !isVisible || !pos ? null : ( <> - + ; @@ -57,9 +62,10 @@ export default function Polygon({ id, isVisible, property, geometry, layer, feat [stroke, strokeColor], ); const memoFillColor = useMemo(() => (fill ? toColor(fillColor) : undefined), [fill, fillColor]); + const availability = useMemo(() => toTimeInterval(feature?.interval), [feature?.interval]); return !isVisible ? null : ( - + ; @@ -35,9 +40,10 @@ export default function Polyline({ id, isVisible, property, geometry, layer, fea isEqual, ); const material = useMemo(() => toColor(strokeColor), [strokeColor]); + const availability = useMemo(() => toTimeInterval(feature?.interval), [feature?.interval]); return !isVisible ? null : ( - + { const alpha = parseInt(m[4] ? m[4].repeat(2) : m[2], 16) / 255; return Color.fromCssColorString(`#${m[1] ?? m[3]}`).withAlpha(alpha); }; + +export const toTimeInterval = ( + interval: TimeInterval | undefined, +): CesiumTimeIntervalCollection | undefined => { + if (!interval) { + return; + } + return new CesiumTimeIntervalCollection([ + new CesiumTimeInterval({ + start: JulianDate.fromDate(interval[0]), + stop: interval[1] ? JulianDate.fromDate(interval[1]) : Iso8601.MAXIMUM_VALUE, + }), + ]); +}; diff --git a/src/core/mantle/evaluator/simple/index.ts b/src/core/mantle/evaluator/simple/index.ts index 0056904347..054da169fd 100644 --- a/src/core/mantle/evaluator/simple/index.ts +++ b/src/core/mantle/evaluator/simple/index.ts @@ -9,11 +9,13 @@ import { LayerAppearanceTypes, LayerSimple, ExpressionContainer, + TimeInterval, } from "../../types"; import { defined } from "../../utils"; import { ConditionalExpression } from "./conditionalExpression"; import { Expression } from "./expression"; +import { evalTimeInterval } from "./interval"; export async function evalSimpleLayer( layer: LayerSimple, @@ -21,19 +23,25 @@ export async function evalSimpleLayer( ): Promise { const features = layer.data ? await ctx.getAllFeatures(layer.data) : undefined; const appearances: Partial = pick(layer, appearanceKeys); + const timeIntervals = evalTimeInterval(features, layer.data?.time); return { layer: evalLayerAppearances(appearances, layer), - features: features?.map(f => evalSimpleLayerFeature(layer, f)), + features: features?.map((f, i) => evalSimpleLayerFeature(layer, f, timeIntervals?.[i])), }; } -export const evalSimpleLayerFeature = (layer: LayerSimple, feature: Feature): ComputedFeature => { +export const evalSimpleLayerFeature = ( + layer: LayerSimple, + feature: Feature, + interval?: TimeInterval, +): ComputedFeature => { const appearances: Partial = pick(layer, appearanceKeys); const nextFeature = evalJsonProperties(layer, feature); return { ...nextFeature, ...evalLayerAppearances(appearances, layer, nextFeature), type: "computedFeature", + interval, }; }; diff --git a/src/core/mantle/evaluator/simple/interval.test.ts b/src/core/mantle/evaluator/simple/interval.test.ts new file mode 100644 index 0000000000..cfa76affc8 --- /dev/null +++ b/src/core/mantle/evaluator/simple/interval.test.ts @@ -0,0 +1,102 @@ +import { expect, test } from "vitest"; + +import { evalTimeInterval } from "./interval"; + +test("evalTimeInterval", () => { + expect( + evalTimeInterval( + [ + { + type: "feature", + id: "a", + properties: { + time: "2023-02-03T00:00:00.000Z", + }, + }, + ], + { property: "time" }, + ), + ).toEqual([[new Date("2023-02-03T00:00:00.000Z"), undefined]]); + + expect( + evalTimeInterval( + [ + { + type: "feature", + id: "d", + properties: { + time: "2024-02-03T00:00:00.000Z", + }, + }, + { + type: "feature", + id: "b", + properties: { + time: "2023-02-04T00:00:00.000Z", + }, + }, + { + type: "feature", + id: "a", + properties: { + time: "2023-02-03T00:00:00.000Z", + }, + }, + { + type: "feature", + id: "c", + properties: { + time: "2023-02-05T00:00:00.000Z", + }, + }, + ], + { property: "time" }, + ), + ).toEqual([ + [new Date("2023-02-03T00:00:00.000Z"), new Date("2023-02-04T00:00:00.000Z")], + [new Date("2023-02-04T00:00:00.000Z"), new Date("2023-02-05T00:00:00.000Z")], + [new Date("2023-02-05T00:00:00.000Z"), new Date("2024-02-03T00:00:00.000Z")], + [new Date("2024-02-03T00:00:00.000Z"), undefined], + ]); + + expect( + evalTimeInterval( + [ + { + type: "feature", + id: "d", + properties: { + time: "2024-02-03T00:00:00.000Z", + }, + }, + { + type: "feature", + id: "b", + properties: { + time: "2023-02-04T00:00:00.000Z", + }, + }, + { + type: "feature", + id: "a", + properties: { + time: "2023-02-03T00:00:00.000Z", + }, + }, + { + type: "feature", + id: "c", + properties: { + time: "2023-02-05T00:00:00.000Z", + }, + }, + ], + { property: "time", interval: 1000 }, + ), + ).toEqual([ + [new Date("2023-02-03T00:00:00.000Z"), new Date("2023-02-03T00:00:01.000Z")], + [new Date("2023-02-04T00:00:00.000Z"), new Date("2023-02-04T00:00:01.000Z")], + [new Date("2023-02-05T00:00:00.000Z"), new Date("2023-02-05T00:00:01.000Z")], + [new Date("2024-02-03T00:00:00.000Z"), new Date("2024-02-03T00:00:01.000Z")], + ]); +}); diff --git a/src/core/mantle/evaluator/simple/interval.ts b/src/core/mantle/evaluator/simple/interval.ts new file mode 100644 index 0000000000..3bb59e699f --- /dev/null +++ b/src/core/mantle/evaluator/simple/interval.ts @@ -0,0 +1,23 @@ +import { Data, Feature, TimeInterval } from "../../types"; + +export function evalTimeInterval( + features: Feature[] | undefined, + time: Data["time"], +): TimeInterval[] | void { + if (!features || !time?.property) { + return; + } + const startTimes: Date[] = []; + for (const feature of features) { + const property = feature.properties[time.property]; + if (!property) { + continue; + } + startTimes.push(new Date(property)); + } + startTimes.sort((a, b) => a.getTime() - b.getTime()); + return startTimes.map((start, i) => [ + start, + time.interval ? new Date(start.getTime() + time.interval) : startTimes[i + 1], + ]); +} diff --git a/src/core/mantle/types/index.ts b/src/core/mantle/types/index.ts index 34d23a3f39..5b6029ed55 100644 --- a/src/core/mantle/types/index.ts +++ b/src/core/mantle/types/index.ts @@ -63,6 +63,10 @@ export type Data = { value?: any; layers?: string | string[]; jsonProperties?: string[]; + time?: { + property?: string; + interval?: number; // milliseconds + }; csv?: { idColumn?: string | number; latColumn?: string | number; @@ -91,11 +95,14 @@ export type DataType = | "shapefile" | "gtfs"; +export type TimeInterval = [start: Date, end?: Date]; + // Feature export type CommonFeature = { type: T; id: string; geometry?: Geometry; + interval?: TimeInterval; properties?: any; range?: DataRange; };