terriajs
Version:
Geospatial data visualization platform.
515 lines (463 loc) • 17.4 kB
text/typescript
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>`;
}