UNPKG

terriajs

Version:

Geospatial data visualization platform.

515 lines (463 loc) 17.4 kB
import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; import Color from "terriajs-cesium/Source/Core/Color"; import Iso8601 from "terriajs-cesium/Source/Core/Iso8601"; import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; import Packable from "terriajs-cesium/Source/Core/Packable"; import TimeInterval from "terriajs-cesium/Source/Core/TimeInterval"; import TimeIntervalCollection from "terriajs-cesium/Source/Core/TimeIntervalCollection"; import BillboardGraphics from "terriajs-cesium/Source/DataSources/BillboardGraphics"; import ColorMaterialProperty from "terriajs-cesium/Source/DataSources/ColorMaterialProperty"; import ConstantPositionProperty from "terriajs-cesium/Source/DataSources/ConstantPositionProperty"; import ConstantProperty from "terriajs-cesium/Source/DataSources/ConstantProperty"; import LabelGraphics from "terriajs-cesium/Source/DataSources/LabelGraphics"; import PathGraphics from "terriajs-cesium/Source/DataSources/PathGraphics"; import PointGraphics from "terriajs-cesium/Source/DataSources/PointGraphics"; import PolylineGlowMaterialProperty from "terriajs-cesium/Source/DataSources/PolylineGlowMaterialProperty"; import SampledPositionProperty from "terriajs-cesium/Source/DataSources/SampledPositionProperty"; import SampledProperty from "terriajs-cesium/Source/DataSources/SampledProperty"; import TimeIntervalCollectionPositionProperty from "terriajs-cesium/Source/DataSources/TimeIntervalCollectionPositionProperty"; import TimeIntervalCollectionProperty from "terriajs-cesium/Source/DataSources/TimeIntervalCollectionProperty"; import HeightReference from "terriajs-cesium/Source/Scene/HeightReference"; import TerriaFeature from "../Models/Feature/Feature"; import { getRowValues } from "./createLongitudeLatitudeFeaturePerRow"; import { getFeatureStyle, SupportedBillboardGraphics, SupportedLabelGraphics, SupportedPathGraphics, SupportedPointGraphics, SupportedPolylineGlowMaterial, SupportedSolidColorMaterial } from "./getFeatureStyle"; import TableColumn from "./TableColumn"; import TableColumnType from "./TableColumnType"; import TableStyle from "./TableStyle"; type RequiredTableStyle = TableStyle & { longitudeColumn: TableColumn; latitudeColumn: TableColumn; timeColumn: TableColumn; idColumns: TableColumn[]; timeIntervals: (JulianDate | null)[]; }; type TimeProperties<T> = { [key in keyof T]: PreSampledProperty | TimeIntervalCollectionProperty; }; type ResolvedTimeProperties<T> = { [key in keyof T]?: | SampledProperty | TimeIntervalCollectionProperty | ConstantProperty; }; /** For a given TimeProperties object, convert all PreSampledProperty to SampledProperty */ function convertPreSampledProperties<T>( timeProperties: TimeProperties<T> | undefined ): ResolvedTimeProperties<T> { if (!timeProperties) return {}; return Object.entries(timeProperties).reduce<ResolvedTimeProperties<T>>( (current, [key, value]) => { if (value instanceof PreSampledProperty) { const sampledProperty = value.getProperty(); if (sampledProperty) { current[key as keyof T] = sampledProperty; } } else if (value instanceof TimeIntervalCollectionProperty) { current[key as keyof T] = value; } return current; }, {} ); } /** This class can be used in-place for Cesium's SampledProperty. * * It provides better performance as instead of calling `SampledProperty.addSample` for every sample, it will call `SampledProperty.addSamples` once with all samples. * This occurs when `PreSampledProperty.toSampledProperty()` is called **/ class PreSampledProperty { private times: JulianDate[] = []; private values: Packable[] = []; private allValuesAreTheSame = true; constructor(private readonly type: Packable) {} getProperty() { if (this.allValuesAreTheSame) { return new ConstantProperty(this.values[0]); } const property = new SampledProperty(this.type); property.addSamples(this.times, this.values); return property; } addSample(time: JulianDate, value: Packable) { this.times.push(time); if ( this.allValuesAreTheSame && this.values.length > 1 && value.toString() !== this.values[this.values.length - 2].toString() ) { this.allValuesAreTheSame = false; } this.values.push(value); } } /** This class can be used in-place for Cesium's SampledPositionProperty. * * It behaves exactly the same as PreSampledProperty **/ class PreSampledPositionProperty { private times: JulianDate[] = []; private values: Cartesian3[] = []; private allValuesAreTheSame = true; constructor() {} getProperty() { if (this.allValuesAreTheSame) { return new ConstantPositionProperty(this.values[0]); } const property = new SampledPositionProperty(); property.addSamples(this.times, this.values); return property; } addSample(time: JulianDate, value: Cartesian3) { this.times.push(time); if ( this.allValuesAreTheSame && this.values.length > 1 && !value.equals(this.values[this.values.length - 2]) ) { this.allValuesAreTheSame = false; } this.values.push(value); } } /** * Create lat/lon features, one for each id group in the table */ export default function createLongitudeLatitudeFeaturePerId( style: RequiredTableStyle ): TerriaFeature[] { const features: TerriaFeature[] = []; for (let i = 0; i < style.rowGroups.length; i++) { const [featureId, rowIds] = style.rowGroups[i]; features.push(createFeature(featureId, rowIds, style)); } return features; } function createProperty(type: Packable, interpolate: boolean) { return interpolate ? new PreSampledProperty(type) : new TimeIntervalCollectionProperty(); } function createFeature( featureId: string, rowIds: number[], style: RequiredTableStyle ): TerriaFeature { const isSampled = !!style.isSampled; const tableHasScalarColumn = !!style.tableModel.tableColumns.find( (col) => col.type === TableColumnType.scalar ); const interpolate = isSampled && tableHasScalarColumn; const positionProperty = isSampled ? new PreSampledPositionProperty() : new TimeIntervalCollectionPositionProperty(); // The following "TimeProperties<T>" objects are used to transform feature styling properties into time-enabled properties (eg SampledProperty or TimeIntervalCollectionProperty) // Required<T> is used as we need to make sure that all styling properties have a time-enabled property defined // See `getFeatureStyle` for "raw" feature styling properties const pointGraphicsTimeProperties: | TimeProperties<Required<SupportedPointGraphics>> | undefined = style.pointStyleMap.traits.enabled ? { color: createProperty(Color, interpolate), outlineColor: createProperty(Color, interpolate), pixelSize: createProperty(Number, interpolate), outlineWidth: createProperty(Number, interpolate) } : undefined; const billboardGraphicsTimeProperties: | TimeProperties<Required<SupportedBillboardGraphics>> | undefined = style.pointStyleMap.traits.enabled ? { image: new TimeIntervalCollectionProperty(), height: createProperty(Number, interpolate), width: createProperty(Number, interpolate), color: createProperty(Color, interpolate), rotation: createProperty(Number, interpolate), pixelOffset: createProperty(Cartesian2, interpolate) } : undefined; const pathGraphicsTimeProperties: | TimeProperties<Required<SupportedPathGraphics>> | undefined = style.trailStyleMap.traits.enabled ? { leadTime: createProperty(Number, interpolate), trailTime: createProperty(Number, interpolate), width: createProperty(Number, interpolate), resolution: createProperty(Number, interpolate) } : undefined; const pathGraphicsSolidColorTimeProperties: | TimeProperties<Required<SupportedSolidColorMaterial>> | undefined = style.trailStyleMap.traits.enabled && style.trailStyleMap.traits.materialType === "solidColor" ? { color: createProperty(Color, interpolate) } : undefined; const pathGraphicsPolylineGlowTimeProperties: | TimeProperties<Required<SupportedPolylineGlowMaterial>> | undefined = style.trailStyleMap.traits.enabled && style.trailStyleMap.traits.materialType === "polylineGlow" ? { color: createProperty(Color, interpolate), glowPower: createProperty(Number, interpolate), taperPower: createProperty(Number, interpolate) } : undefined; const labelGraphicsTimeProperties: | TimeProperties<Required<SupportedLabelGraphics>> | undefined = style.labelStyleMap.traits.enabled ? { font: new TimeIntervalCollectionProperty(), text: new TimeIntervalCollectionProperty(), style: new TimeIntervalCollectionProperty(), scale: createProperty(Number, interpolate), fillColor: createProperty(Color, interpolate), outlineColor: createProperty(Color, interpolate), outlineWidth: createProperty(Number, interpolate), pixelOffset: createProperty(Cartesian2, interpolate), verticalOrigin: new TimeIntervalCollectionProperty(), horizontalOrigin: new TimeIntervalCollectionProperty() } : undefined; const properties = new TimeIntervalCollectionProperty(); const description = new TimeIntervalCollectionProperty(); const longitudes = style.longitudeColumn.valuesAsNumbers.values; const latitudes = style.latitudeColumn.valuesAsNumbers.values; const timeIntervals = style.timeIntervals; const availability = new TimeIntervalCollection(); const tableColumns = style.tableModel.tableColumns; /** use `PointGraphics` or `BillboardGraphics`. This wil be false if any pointTraits.marker !== "point", as then we use images as billboards */ let usePointGraphicsForId = true; for (let i = 0; i < rowIds.length; i++) { const rowId = rowIds[i]; const longitude = longitudes[rowId]; const latitude = latitudes[rowId]; const interval = timeIntervals[rowId]; if (longitude === null || latitude === null || !interval) { continue; } addSampleOrInterval( positionProperty, Cartesian3.fromDegrees(longitude, latitude, 0.0), interval ); const { pointGraphicsOptions, usePointGraphics, pathGraphicsOptions, pathGraphicsPolylineGlowOptions, pathGraphicsSolidColorOptions, labelGraphicsOptions, billboardGraphicsOptions } = getFeatureStyle(style, rowId); if (!usePointGraphics) { usePointGraphicsForId = false; } if (pointGraphicsTimeProperties && pointGraphicsOptions) // Copy all style object values across to time-enabled properties Object.entries(pointGraphicsOptions).forEach(([key, value]) => { if (key in pointGraphicsTimeProperties) addSampleOrInterval( pointGraphicsTimeProperties[key as keyof SupportedPointGraphics], value, interval ); }); if (billboardGraphicsTimeProperties && billboardGraphicsOptions) Object.entries(billboardGraphicsOptions).forEach(([key, value]) => { if (key in billboardGraphicsTimeProperties) addSampleOrInterval( billboardGraphicsTimeProperties[ key as keyof SupportedBillboardGraphics ], value, interval ); }); if (labelGraphicsTimeProperties && labelGraphicsOptions) Object.entries(labelGraphicsOptions).forEach(([key, value]) => { if (key in labelGraphicsTimeProperties) addSampleOrInterval( labelGraphicsTimeProperties[key as keyof SupportedLabelGraphics], value, interval ); }); if (pathGraphicsTimeProperties && pathGraphicsOptions) Object.entries(pathGraphicsOptions).forEach(([key, value]) => { if (key in pathGraphicsTimeProperties) addSampleOrInterval( pathGraphicsTimeProperties[key as keyof SupportedPathGraphics], value, interval ); }); if (pathGraphicsSolidColorTimeProperties && pathGraphicsSolidColorOptions) Object.entries(pathGraphicsSolidColorOptions).forEach(([key, value]) => { if (key in pathGraphicsSolidColorTimeProperties) addSampleOrInterval( pathGraphicsSolidColorTimeProperties[ key as keyof SupportedSolidColorMaterial ], value, interval ); }); if ( pathGraphicsPolylineGlowTimeProperties && pathGraphicsPolylineGlowOptions ) Object.entries(pathGraphicsPolylineGlowOptions).forEach( ([key, value]) => { if (key in pathGraphicsPolylineGlowTimeProperties) addSampleOrInterval( pathGraphicsPolylineGlowTimeProperties[ key as keyof SupportedPolylineGlowMaterial ], value, interval ); } ); // Feature properties/description addSampleOrInterval( properties, { ...getRowValues(rowId, tableColumns) }, interval ); addSampleOrInterval( description, getRowDescription(rowId, tableColumns), interval ); availability.addInterval(interval); } const show = calculateShow(availability); const feature = new TerriaFeature({ position: // positionProperty is either SampledPositionProperty or PreSampledPositionProperty // If it's PreSampledPositionProperty - we need to transform it to SampledPositionProperty by calling `getProperty()` positionProperty instanceof PreSampledPositionProperty ? positionProperty.getProperty() : positionProperty, point: usePointGraphicsForId ? new PointGraphics({ ...convertPreSampledProperties(pointGraphicsTimeProperties), show, heightReference: HeightReference.CLAMP_TO_GROUND }) : undefined, billboard: !usePointGraphicsForId ? new BillboardGraphics({ ...convertPreSampledProperties(billboardGraphicsTimeProperties), heightReference: HeightReference.CLAMP_TO_GROUND, show }) : undefined, path: pathGraphicsTimeProperties ? new PathGraphics({ show, ...convertPreSampledProperties(pathGraphicsTimeProperties), // Material has to be handled separately from pathGraphicsTimeProperties material: pathGraphicsPolylineGlowTimeProperties ? new PolylineGlowMaterialProperty( convertPreSampledProperties( pathGraphicsPolylineGlowTimeProperties ) ) : pathGraphicsSolidColorTimeProperties ? new ColorMaterialProperty( convertPreSampledProperties( pathGraphicsSolidColorTimeProperties ).color ) : undefined }) : undefined, label: labelGraphicsTimeProperties ? new LabelGraphics({ show, ...convertPreSampledProperties(labelGraphicsTimeProperties) }) : undefined, availability }); // Add properties to feature.data so we have access to TimeIntervalCollectionProperty outside of the PropertyBag. feature.data = { timeIntervalCollection: properties, rowIds, type: "terriaFeatureData" }; feature.description = description; return feature; } function addSampleOrInterval( property: | PreSampledProperty | PreSampledPositionProperty | TimeIntervalCollectionProperty | TimeIntervalCollectionPositionProperty, data: any, interval: TimeInterval ) { if ( property instanceof PreSampledProperty || property instanceof PreSampledPositionProperty ) { property.addSample(interval.start, data); } else { const thisInterval = interval.clone(); thisInterval.data = data; property?.intervals.addInterval(thisInterval); } } function calculateShow(availability: TimeIntervalCollection) { const show = new TimeIntervalCollectionProperty(); if (availability.start) { const start = availability.start; const stop = availability.stop; show.intervals.addInterval( new TimeInterval({ start: <any>Iso8601.MINIMUM_VALUE, stop: <any>Iso8601.MAXIMUM_VALUE, data: false }) ); show.intervals.addInterval(new TimeInterval({ start, stop, data: true })); } else { show.intervals.addInterval( new TimeInterval({ start: <any>Iso8601.MINIMUM_VALUE, stop: <any>Iso8601.MAXIMUM_VALUE, data: true }) ); } return show; } function getRowDescription( index: number, tableColumns: Readonly<TableColumn[]> ) { const rows = tableColumns .map((column) => { const title = column.title; const value = column.valueFunctionForType(index); return `<tr><td>${title}</td><td>${value}</td></tr>`; }) .join("\n"); return `<table class="cesium-infoBox-defaultTable">${rows}</table>`; }