terriajs
Version:
Geospatial data visualization platform.
993 lines • 59.3 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import bbox from "@turf/bbox";
import i18next from "i18next";
import { action, computed, makeObservable, observable, onBecomeObserved, onBecomeUnobserved, override, reaction, runInAction, toJS } from "mobx";
import { createTransformer } from "mobx-utils";
import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3";
import clone from "terriajs-cesium/Source/Core/clone";
import Color from "terriajs-cesium/Source/Core/Color";
import DeveloperError from "terriajs-cesium/Source/Core/DeveloperError";
import Iso8601 from "terriajs-cesium/Source/Core/Iso8601";
import JulianDate from "terriajs-cesium/Source/Core/JulianDate";
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 ConstantProperty from "terriajs-cesium/Source/DataSources/ConstantProperty";
import CustomDataSource from "terriajs-cesium/Source/DataSources/CustomDataSource";
import CzmlDataSource from "terriajs-cesium/Source/DataSources/CzmlDataSource";
import Entity from "terriajs-cesium/Source/DataSources/Entity";
import GeoJsonDataSource from "terriajs-cesium/Source/DataSources/GeoJsonDataSource";
import PointGraphics from "terriajs-cesium/Source/DataSources/PointGraphics";
import PolylineGraphics from "terriajs-cesium/Source/DataSources/PolylineGraphics";
import HeightReference from "terriajs-cesium/Source/Scene/HeightReference";
import filterOutUndefined from "../Core/filterOutUndefined";
import formatPropertyValue from "../Core/formatPropertyValue";
import { explodeMultiPoint, isPoint } from "../Core/GeoJson";
import hashFromString from "../Core/hashFromString";
import isDefined from "../Core/isDefined";
import { isJsonArray, isJsonNumber, isJsonObject } from "../Core/Json";
import { isJson } from "../Core/loadBlob";
import StandardCssColors from "../Core/StandardCssColors";
import TerriaError, { networkRequestError } from "../Core/TerriaError";
import ProtomapsImageryProvider from "../Map/ImageryProvider/ProtomapsImageryProvider";
import { ProtomapsGeojsonSource } from "../Map/Vector/Protomaps/ProtomapsGeojsonSource";
import { tableStyleToProtomaps } from "../Map/Vector/Protomaps/tableStyleToProtomaps";
import Reproject from "../Map/Vector/Reproject";
import CatalogMemberMixin from "../ModelMixins/CatalogMemberMixin";
import UrlMixin from "../ModelMixins/UrlMixin";
import proxyCatalogItemUrl from "../Models/Catalog/proxyCatalogItemUrl";
import createStratumInstance from "../Models/Definition/createStratumInstance";
import LoadableStratum from "../Models/Definition/LoadableStratum";
import StratumOrder from "../Models/Definition/StratumOrder";
import TerriaFeature from "../Models/Feature/Feature";
import TableStylingWorkflow from "../Models/Workflows/TableStylingWorkflow";
import createLongitudeLatitudeFeaturePerRow from "../Table/createLongitudeLatitudeFeaturePerRow";
import TableAutomaticStylesStratum from "../Table/TableAutomaticStylesStratum";
import { createRowGroupId } from "../Table/TableStyle";
import { GeoJsonTraits } from "../Traits/TraitsClasses/GeoJsonTraits";
import { RectangleTraits } from "../Traits/TraitsClasses/MappableTraits";
import FeatureInfoUrlTemplateMixin from "./FeatureInfoUrlTemplateMixin";
import { isDataSource } from "./MappableMixin";
import TableMixin from "./TableMixin";
import PinBuilder from "terriajs-cesium/Source/Core/PinBuilder";
import VerticalOrigin from "terriajs-cesium/Source/Scene/VerticalOrigin";
export const FEATURE_ID_PROP = "_id_";
const SIMPLE_STYLE_KEYS = [
"marker-size",
"marker-color",
"marker-symbol",
"marker-opacity",
"marker-url",
"stroke",
"stroke-opacity",
"stroke-width",
"marker-stroke-width",
"polyline-stroke-width",
"polygon-stroke-width",
"fill",
"fill-opacity"
];
class GeoJsonStratum extends LoadableStratum(GeoJsonTraits) {
_item;
static stratumName = "geojson";
constructor(_item) {
super();
this._item = _item;
makeObservable(this);
}
duplicateLoadableStratum(newModel) {
return new GeoJsonStratum(newModel);
}
static load(item) {
return new GeoJsonStratum(item);
}
get rectangle() {
if (this._item._readyData) {
try {
const geojsonBbox = bbox(this._item._readyData);
return createStratumInstance(RectangleTraits, {
west: geojsonBbox[0],
south: geojsonBbox[1],
east: geojsonBbox[2],
north: geojsonBbox[3]
});
}
catch (e) {
TerriaError.from(e, "Failed to create `rectangle` for GeoJSON").log();
}
}
}
get opacity() {
return 1;
}
get disableSplitter() {
// Disable splitter if mapItems has any datasources
return this._item.mapItems.find(isDataSource) ? true : undefined;
}
get disableOpacityControl() {
// Disable opacity if mapItems has any datasources
return this._item.mapItems.find(isDataSource) ? true : undefined;
}
get showDisableStyleOption() {
return true;
}
get forceCesiumPrimitives() {
// Disable TableStyling for the following:
// If MultiPoint features exist
// If more than 50% of features have simple style properties - disable table styling
if (this._item.featureCounts.multiPoint > 0 ||
this._item.featureCounts.simpleStyle / this._item.featureCounts.total >=
0.5) {
return true;
}
}
}
__decorate([
computed
], GeoJsonStratum.prototype, "rectangle", null);
__decorate([
computed
], GeoJsonStratum.prototype, "disableSplitter", null);
__decorate([
computed
], GeoJsonStratum.prototype, "disableOpacityControl", null);
__decorate([
computed
], GeoJsonStratum.prototype, "forceCesiumPrimitives", null);
StratumOrder.addLoadStratum(GeoJsonStratum.stratumName);
function GeoJsonMixin(Base) {
class GeoJsonMixin extends TableMixin(FeatureInfoUrlTemplateMixin(UrlMixin(Base))) {
_dataSource;
_imageryProvider;
tableStyleReactionDisposer;
/** Geojson FeatureCollection in WGS84 */
_readyData;
/** Number of features in _readyData FeatureCollection */
featureCounts = {
point: 0,
multiPoint: 0,
line: 0,
polygon: 0,
simpleStyle: 0,
total: 0
};
constructor(...args) {
super(...args);
makeObservable(this);
// Add GeoJsonStratum
if (this.strata.get(GeoJsonStratum.stratumName) === undefined) {
runInAction(() => {
this.strata.set(GeoJsonStratum.stratumName, GeoJsonStratum.load(this));
});
}
// Add TableAutomaticStylesStratum
if (this.strata.get(TableAutomaticStylesStratum.stratumName) === undefined) {
this.strata.set(TableAutomaticStylesStratum.stratumName, new TableAutomaticStylesStratum(this));
}
// Setup table style reactions
// We should only update geojson table styling when our map items have consumers
onBecomeObserved(this, "mapItems", this.startTableStyleReaction.bind(this));
onBecomeUnobserved(this, "mapItems", this.stopTableStyleReaction.bind(this));
}
startTableStyleReaction() {
if (!this.tableStyleReactionDisposer) {
// Update protomaps imagery provider if activeTableStyle changes
this.tableStyleReactionDisposer = reaction(() => getStyleReactiveDependencies(this), () => {
if (this._imageryProvider &&
this.readyData &&
this.useTableStylingAndProtomaps) {
runInAction(() => {
this._imageryProvider = this.createProtomapsImageryProvider(this.readyData);
});
}
},
// Fire immediately, just in case reactions change while not observing mapItems
{ fireImmediately: true });
}
}
stopTableStyleReaction() {
if (this.tableStyleReactionDisposer) {
this.tableStyleReactionDisposer();
this.tableStyleReactionDisposer = undefined;
}
}
get isGeoJson() {
return true;
}
get name() {
if (CatalogMemberMixin.isMixedInto(this.sourceReference)) {
return super.name || this.sourceReference.name;
}
return super.name;
}
get cacheDuration() {
if (isDefined(super.cacheDuration)) {
return super.cacheDuration;
}
return "1d";
}
/**
* Returns the final raw data after all transformations are applied.
* (Geojson FeatureCollection in WGS84)
*/
get readyData() {
return this._readyData;
}
get _canExportData() {
return isDefined(this.readyData);
}
async _exportData() {
if (isDefined(this.readyData)) {
let name = this.name || this.uniqueId || "data.geojson";
if (!isJson(name)) {
name = `${name}.geojson`;
}
return {
name,
file: new Blob([JSON.stringify(this.readyData)])
};
}
throw new TerriaError({
sender: this,
message: "No data available to download."
});
}
get mapItems() {
if (this.isLoadingMapItems) {
return [];
}
if (this._dataSource) {
this._dataSource.show = this.show;
}
let points = undefined;
if (this.useTableStylingAndProtomaps) {
const pts = this.createPoints(this.activeTableStyle);
if (pts && pts.entities.values.length !== 0) {
points = pts;
points.show = this.show;
}
}
return filterOutUndefined([
points,
this._dataSource,
this._imageryProvider
? {
imageryProvider: this._imageryProvider,
show: this.show,
alpha: this.opacity,
clippingRectangle: undefined
}
: undefined
]);
}
/**
* {@link FeatureInfoUrlTemplateMixin.buildFeatureFromPickResult}
*/
buildFeatureFromPickResult(_screenPosition, pickResult) {
if (pickResult instanceof Entity) {
return TerriaFeature.fromEntityCollectionOrEntity(pickResult);
}
else if (isDefined(pickResult?.id)) {
return TerriaFeature.fromEntityCollectionOrEntity(pickResult.id);
}
}
/** Only use MapboxVectorTiles (through geojson-vt and protomaps.js) if enabled and not using unsupported traits
* For more info see GeoJsonMixin.forceLoadMapItems
*/
get useTableStylingAndProtomaps() {
return (!this.forceCesiumPrimitives &&
!isDefined(this.czmlTemplate) &&
// Table styling doesn't support the old GeoJson StyleTraits
Object.keys(this.style.traits).every((styleTrait) => !isDefined(this.style[styleTrait])) &&
!isDefined(this.timeProperty) &&
!isDefined(this.heightProperty) &&
(!isDefined(this.perPropertyStyles) ||
this.perPropertyStyles.length === 0));
}
/** Remove chart items from TableMixin.chartItems */
get chartItems() {
return [];
}
/** GeojsonMixin has 3 rendering modes:
* - CZML:
* - if `czmlTemplate` is defined (see `GeoJsonTraits.czmlTemplate`)
* - Table styling / Mapbox vector tiles (through geojson-vt and protomaps.js)
* - Will be used by default, if not using unsupported traits (see below)
* - Cesium primitives if:
* - `GeoJsonTraits.forceCesiumPrimitives = true`
* - Using `timeProperty` or `heightProperty` or `perPropertyStyles` or simple-style `marker-symbol`
* - More than 50% of GeoJSON features have simply-style properties (eg "fill-color")
* - MultiPoint features are in GeoJSON (not supported by Table styling)
*/
async forceLoadMapItems() {
const czmlTemplate = this.czmlTemplate;
const filterByProperties = this.filterByProperties;
const explodeMultiPoints = this.explodeMultiPoints;
let geoJson;
try {
geoJson = await this.forceLoadGeojsonData();
if (geoJson === undefined) {
return;
}
const geoJsonWgs84 = await reprojectToGeographic(geoJson, this.terria.configParameters.proj4ServiceBaseUrl);
const featureCounts = {
point: 0,
multiPoint: 0,
line: 0,
polygon: 0,
simpleStyle: 0,
total: 0
};
// We will re-add features depending if filterByProperties - or geometry is invalid
const features = geoJsonWgs84.features;
geoJsonWgs84.features = [];
let currentFeatureId = 0;
for (let i = 0; i < features.length; i++) {
const feature = features[i];
// Ignore features without geometry or type
if (!isJsonObject(feature.geometry, false) || !feature.geometry.type)
continue;
// Ignore features with invalid coordinates
if (!isJsonArray(feature.geometry.coordinates, false) ||
feature.geometry.coordinates.length === 0)
continue;
if (!feature.properties) {
feature.properties = {};
}
// Filter features by `featureFilterByProps` trait if defined
if (filterByProperties &&
!Object.entries(filterByProperties).every(([key, value]) => feature.properties[key] === value)) {
continue;
}
if (explodeMultiPoints && feature.geometry.type === "MultiPoint") {
// Replace the MultiPoint with equivalent Point features and repeat
// the iteration to pick up the exploded features.
features.splice(i, 1, ...explodeMultiPoint(feature));
i--;
continue;
}
geoJsonWgs84.features.push(feature);
// Add feature index to FEATURE_ID_PROP ("_id_") feature property
// This is used to refer to each feature in TableMixin (as row ID)
const properties = feature.properties;
properties[FEATURE_ID_PROP] = currentFeatureId;
// Count features types
if (feature.geometry.type === "Point") {
featureCounts.point++;
}
else if (feature.geometry.type === "MultiPoint") {
featureCounts.multiPoint++;
}
else if (feature.geometry.type === "LineString" ||
feature.geometry.type === "MultiLineString") {
featureCounts.line++;
}
else if (feature.geometry.type === "Polygon" ||
feature.geometry.type === "MultiPolygon") {
featureCounts.polygon++;
}
// Does feature include simplestyle-spec properties (eg "fill-colour)")
if (SIMPLE_STYLE_KEYS.find((key) => properties[key])) {
featureCounts.simpleStyle++;
}
featureCounts.total++;
// Note it is important to increment currentFeatureId only if we are including the feature - as this needs to match the row ID in TableMixin (through dataColumnMajor)
currentFeatureId++;
}
runInAction(() => {
this.featureCounts = featureCounts;
if (featureCounts.total === 0) {
this._readyData = undefined;
}
else {
this._readyData = geoJsonWgs84;
}
});
if (isDefined(czmlTemplate)) {
const dataSource = await this.loadCzmlDataSource(geoJsonWgs84);
runInAction(() => {
this._dataSource = dataSource;
this._imageryProvider = undefined;
});
}
else if (runInAction(() => this.useTableStylingAndProtomaps)) {
runInAction(() => {
this._imageryProvider =
this.createProtomapsImageryProvider(geoJsonWgs84);
});
}
else {
const dataSource = await this.loadGeoJsonDataSource(geoJsonWgs84);
if (this.clustering.enabled) {
const pinBackgroundColor = this.clustering.pinBackgroundColor;
const pinSize = this.clustering.pinSize;
const pinBuilder = new PinBuilder();
dataSource.clustering.enabled = true;
dataSource.clustering.pixelRange = this.clustering.pixelRange;
dataSource.clustering.minimumClusterSize =
this.clustering.minimumClusterSize;
dataSource.clustering.clusterEvent.addEventListener(function (entities, cluster) {
cluster.label.show = false;
cluster.billboard.verticalOrigin = VerticalOrigin.BOTTOM;
cluster.billboard.image = pinBuilder
.fromText(entities.length.toLocaleString(), Color.fromCssColorString(pinBackgroundColor), pinSize)
.toDataURL();
cluster.billboard.show = true;
});
}
runInAction(() => {
this._dataSource = dataSource;
this._imageryProvider = undefined;
});
}
this._dataSource?.entities.values.forEach((entity) => (entity._catalogItem = this));
}
catch (e) {
throw networkRequestError(TerriaError.from(e, {
title: i18next.t("models.geoJson.errorLoadingTitle"),
message: i18next.t("models.geoJson.errorParsingMessage")
}));
}
}
addPerPropertyStyleToGeoJson(fc) {
for (let i = 0; i < fc.features.length; i++) {
const featureProperties = fc.features[i].properties;
if (featureProperties === null) {
return;
}
const featurePropertiesEntires = Object.entries(featureProperties);
const matchedStyles = this.perPropertyStyles.filter((style) => {
const stylePropertiesEntries = Object.entries(style.properties ?? {});
// For every key-value pair in the style, is there an identical one in the feature's properties?
return stylePropertiesEntries.every(([styleKey, styleValue]) => featurePropertiesEntires.find(([featKey, featValue]) => {
if (typeof styleValue === "string" && !style.caseSensitive) {
return (featKey === styleKey &&
(typeof featValue === "string"
? featValue
: featValue.toString()).toLowerCase() === styleValue.toLowerCase());
}
return featKey === styleKey && featValue === styleValue;
}) !== undefined);
});
if (matchedStyles !== undefined) {
for (const matched of matchedStyles) {
for (const trait of Object.keys(matched.style.traits)) {
featureProperties[trait] =
// @ts-expect-error - TS can't tell that `trait` is of the correct index type for style
matched.style[trait] ?? featureProperties[trait];
}
}
}
}
}
// Create point features using TableMixin.createLongitudeLatitudeFeaturePerRow
// Used with table styling
// Line and Polygon features are handled by Protomaps
createPoints = createTransformer((style) => {
if (!this.readyData)
return;
const latitudes = [];
const longitudes = [];
for (let i = 0; i < this.readyData.features.length; i++) {
const feature = this.readyData.features[i];
if (!isPoint(feature)) {
latitudes.push(null);
longitudes.push(null);
continue;
}
latitudes.push(feature.geometry.coordinates[1]);
longitudes.push(feature.geometry.coordinates[0]);
}
const dataSource = new CustomDataSource(this.name || "Table");
dataSource.entities.suspendEvents();
const features = createLongitudeLatitudeFeaturePerRow(style, longitudes, latitudes);
// _catalogItem property is needed for some feature picking functions (eg FeatureInfoUrlTemplateMixin)
features.forEach((f) => {
f._catalogItem = this;
dataSource.entities.add(f);
});
dataSource.entities.resumeEvents();
return dataSource;
});
createProtomapsImageryProvider(geoJson) {
// Don't need protomaps unless we have lines and polygons to show
// Points are handled by this.createPoints()
if (this.featureCounts.line + this.featureCounts.polygon === 0)
return;
const { paintRules, labelRules, currentTimeRows } = tableStyleToProtomaps(this);
let protomapsData = Object.assign({}, geoJson, {
features: geoJson.features.filter((f) => f.geometry.type !== "Point")
});
// Are we creating a protomaps imagery provider with the same geojson data (readyData)?
// If so we can copy GeojsonSource over to save running geojson-vt again
if (this._imageryProvider instanceof ProtomapsImageryProvider &&
this._imageryProvider.source instanceof ProtomapsGeojsonSource &&
this._imageryProvider.source.geojsonObject === this.readyData) {
protomapsData = this._imageryProvider.source;
}
let provider = new ProtomapsImageryProvider({
terria: this.terria,
data: protomapsData,
id: this.uniqueId,
paintRules,
labelRules,
// Process picked features to add terriaFeatureData (with rowIds)
// This is used by tableFeatureInfoContext to add time-series chart
processPickedFeatures: async (features) => {
if (!currentTimeRows)
return features;
const processedFeatures = [];
features.forEach((f) => {
const rowId = f.properties?.[FEATURE_ID_PROP];
if (isDefined(rowId) && currentTimeRows?.includes(rowId)) {
// To find rowIds for all features in a row group:
// re-create the rowGroupId and then look up in the activeTableStyle.rowGroups
const rowGroupId = createRowGroupId(rowId, this.activeTableStyle.groupByColumns);
const terriaFeatureData = {
...f.data,
type: "terriaFeatureData",
rowIds: this.activeTableStyle.rowGroups.find((group) => group[0] === rowGroupId)?.[1]
};
f.data = terriaFeatureData;
processedFeatures.push(f);
}
});
return processedFeatures;
}
});
provider = this.wrapImageryPickFeatures(provider);
return provider;
}
async loadCzmlDataSource(geoJson) {
const czmlTemplate = runInAction(() => toJS(this.czmlTemplate));
const rootCzml = [
{
id: "document",
name: "CZML",
version: "1.0"
}
];
// Create a czml packet for each geoJson Point/Polygon feature
// For point: set czml position (CartographicDegrees) to point coordinates
// For polygon: set czml positions array (CartographicDegreesListValue) for the `polygon` property
// Set czml properties to feature properties
for (let i = 0; i < geoJson.features.length; i++) {
const feature = geoJson.features[i];
if (feature === null) {
continue;
}
if (feature.geometry?.type === "Point") {
const czml = clone(czmlTemplate ?? {}, true);
const point = feature.geometry;
const coords = point.coordinates;
// Add height = 0 if no height provided
if (coords.length === 2) {
coords[2] = 0;
}
if (isJsonNumber(this.czmlTemplate?.heightOffset)) {
coords[2] += this.czmlTemplate.heightOffset;
}
czml.position = {
cartographicDegrees: point.coordinates
};
czml.properties = Object.assign(czml.properties ?? {}, stringifyFeatureProperties(feature.properties ?? {}));
rootCzml.push(czml);
}
else if ((feature.geometry?.type === "Polygon" ||
feature.geometry?.type === "MultiPolygon") &&
czmlTemplate?.polygon) {
const czml = clone(czmlTemplate ?? {}, true);
// To handle both Polygon and MultiPolygon - transform Polygon coords into MultiPolygon coords
const multiPolygonGeom = feature.geometry?.type === "Polygon"
? [feature.geometry.coordinates]
: feature.geometry.coordinates;
// Loop through Polygons in MultiPolygon
for (let j = 0; j < multiPolygonGeom.length; j++) {
const geom = multiPolygonGeom[j];
const positions = [];
const holes = [];
geom[0].forEach((coords) => {
if (isJsonNumber(this.czmlTemplate?.heightOffset)) {
coords[2] = (coords[2] ?? 0) + this.czmlTemplate.heightOffset;
}
positions.push(coords[0], coords[1], coords[2]);
});
geom.forEach((ring, idx) => {
if (idx === 0)
return;
holes.push(ring.reduce((acc, current) => {
if (isJsonNumber(this.czmlTemplate?.heightOffset)) {
current[2] =
(current[2] ?? 0) + this.czmlTemplate.heightOffset;
}
acc.push(current[0], current[1], current[2]);
return acc;
}, []));
});
czml.polygon.positions = { cartographicDegrees: positions };
czml.polygon.holes = { cartographicDegrees: holes };
czml.properties = Object.assign(czml.properties ?? {}, stringifyFeatureProperties(feature.properties ?? {}));
rootCzml.push(czml);
}
}
else if ((feature?.geometry?.type === "LineString" ||
feature.geometry?.type === "MultiLineString") &&
(czmlTemplate?.polyline ||
czmlTemplate?.polylineVolume ||
czmlTemplate?.wall ||
czmlTemplate?.corridor)) {
const czml = clone(czmlTemplate ?? {}, true);
// To handle both Polygon and MultiPolygon - transform Polygon coords into MultiPolygon coords
const multiLineString = feature.geometry?.type === "LineString"
? [feature.geometry.coordinates]
: feature.geometry.coordinates;
// Loop through Polygons in MultiPolygon
for (let j = 0; j < multiLineString.length; j++) {
const geom = multiLineString[j];
const positions = [];
geom.forEach((coords) => {
if (isJsonNumber(this.czmlTemplate?.heightOffset)) {
coords[2] = (coords[2] ?? 0) + this.czmlTemplate.heightOffset;
}
positions.push(coords[0], coords[1], coords[2]);
});
// Add positions to all CZML line like features
if (czml.polyline) {
czml.polyline.positions = { cartographicDegrees: positions };
}
if (czml.polylineVolume) {
czml.polylineVolume.positions = {
cartographicDegrees: positions
};
}
if (czml.wall) {
czml.wall.positions = { cartographicDegrees: positions };
}
if (czml.corridor) {
czml.corridor.positions = { cartographicDegrees: positions };
}
czml.properties = Object.assign(czml.properties ?? {}, stringifyFeatureProperties(feature.properties ?? {}));
rootCzml.push(czml);
}
}
}
return CzmlDataSource.load(rootCzml);
}
get defaultStyles() {
return {
markerSize: 24,
markerColor: getRandomCssColor(this.name ?? ""),
stroke: getColor(this.terria.baseMapContrastColor),
markerStroke: getColor(this.terria.baseMapContrastColor),
polygonStroke: getColor(this.terria.baseMapContrastColor),
polylineStroke: getRandomCssColor(this.name ?? ""),
markerStrokeWidth: 1,
polylineStrokeWidth: 2,
polygonStrokeWidth: 1,
fill: getRandomCssColor((this.name ?? "") + " fill"),
fillAlpha: 0.75
};
}
/** Applies default values on top of GeoJson StyleTraits. This is only used for Cesium Primitives.*/
get stylesWithDefaults() {
const defaultColor = (colString, defaultColor) => (colString ? getColor(colString) : defaultColor);
const options = {
describe: describeWithoutUnderscores,
markerSize: parseMarkerSize(this.style["marker-size"]) ??
this.defaultStyles.markerSize,
markerSymbol: this.style["marker-symbol"], // and undefined if none
markerColor: defaultColor(this.style["marker-color"], this.defaultStyles.markerColor),
stroke: defaultColor(this.style.stroke, this.defaultStyles.stroke),
polygonStroke: defaultColor(this.style["polygon-stroke"] ?? this.style.stroke, this.defaultStyles.polygonStroke),
// Note these specific stroke widths are only used for geojson-vt
polylineStroke: defaultColor(this.style["polyline-stroke"] ?? this.style.stroke, this.defaultStyles.polylineStroke),
markerStroke: defaultColor(this.style["marker-stroke"] ?? this.style.stroke, this.defaultStyles.markerStroke),
markerStrokeWidth: this.style["marker-stroke-width"] ??
this.style["stroke-width"] ??
this.defaultStyles.markerStrokeWidth,
polylineStrokeWidth: this.style["polyline-stroke-width"] ??
this.style["stroke-width"] ??
this.defaultStyles.polylineStrokeWidth,
polygonStrokeWidth: this.style["polygon-stroke-width"] ??
this.style["stroke-width"] ??
this.defaultStyles.polygonStrokeWidth,
markerOpacity: this.style["marker-opacity"], // not in SimpleStyle spec or supported by Cesium but see below
fill: defaultColor(this.style.fill, this.defaultStyles.fill),
clampToGround: this.clampToGround,
markerUrl: this.style["marker-url"] // not in SimpleStyle spec but gives an alternate to maki marker symbols
? proxyCatalogItemUrl(this, this.style["marker-url"])
: undefined,
credit: this.attribution
};
if (isDefined(this.style["stroke-opacity"])) {
options.stroke.alpha = this.style["stroke-opacity"];
options.polygonStroke.alpha = this.style["stroke-opacity"];
options.polylineStroke.alpha = this.style["stroke-opacity"];
options.markerStroke.alpha = this.style["stroke-opacity"];
}
if (isDefined(this.style["fill-opacity"])) {
options.fill.alpha = this.style["fill-opacity"];
}
else {
options.fill.alpha = this.defaultStyles.fillAlpha;
}
return toJS(options);
}
async loadGeoJsonDataSource(geoJson) {
/* Style information is applied as follows, in decreasing priority:
- simple-style properties set directly on individual features in the GeoJSON file
- simple-style properties set as the 'Style' property on the catalog item
- our 'this.styles' set below (and point styling applied after Cesium loads the GeoJSON)
- if anything is underspecified there, then Cesium's defaults come in.
See https://github.com/mapbox/simplestyle-spec/tree/master/1.1.0
*/
this.addPerPropertyStyleToGeoJson(geoJson);
const now = JulianDate.now();
const styles = runInAction(() => this.stylesWithDefaults);
const dataSource = await GeoJsonDataSource.load(geoJson, styles);
const entities = dataSource.entities;
for (let i = 0; i < entities.values.length; ++i) {
const entity = entities.values[i];
const properties = entity.properties;
// Time
if (isDefined(properties) &&
isDefined(this.timeProperty) &&
isDefined(this.discreteTimesAsSortedJulianDates)) {
const startTimeDiscreteTime = properties[this.timeProperty];
const startTimeIdx = this.discreteTimesAsSortedJulianDates?.findIndex((t) => t.tag === startTimeDiscreteTime.getValue());
const startTime = this.discreteTimesAsSortedJulianDates[startTimeIdx];
if (isDefined(startTime)) {
const endTimeIdx = startTimeIdx + 1;
const endTime = this.discreteTimesAsSortedJulianDates[endTimeIdx];
entity.availability = new TimeIntervalCollection([
new TimeInterval({
start: startTime.time,
stop: endTime?.time ?? Iso8601.MAXIMUM_VALUE,
isStopIncluded: false
})
]);
}
}
// Billboard
if (isDefined(entity.billboard) && isDefined(styles.markerUrl)) {
entity.billboard = new BillboardGraphics({
image: new ConstantProperty(styles.markerUrl),
width: properties && properties["marker-width"]
? new ConstantProperty(properties["marker-width"])
: undefined,
height: properties && properties["marker-height"]
? new ConstantProperty(properties["marker-height"])
: undefined,
rotation: properties && properties["marker-angle"]
? new ConstantProperty(properties["marker-angle"])
: undefined,
heightReference: styles.clampToGround
? new ConstantProperty(HeightReference.RELATIVE_TO_GROUND)
: undefined
});
/* If no marker symbol was provided but Cesium has generated one for a point, then turn it into
a filled circle instead of the default marker. */
}
else if (isDefined(entity.billboard) &&
(!properties || !isDefined(properties["marker-symbol"])) &&
!isDefined(styles.markerSymbol)) {
entity.point = new PointGraphics({
color: new ConstantProperty(getColor(properties?.["marker-color"]?.getValue() ?? styles.markerColor)),
pixelSize: new ConstantProperty(parseMarkerSize(properties && properties["marker-size"]?.getValue()) ?? styles.markerSize / 2),
outlineWidth: new ConstantProperty(properties?.["stroke-width"]?.getValue() ??
styles.markerStrokeWidth),
outlineColor: new ConstantProperty(getColor(properties?.stroke?.getValue() ?? styles.polygonStroke)),
heightReference: new ConstantProperty(styles.clampToGround
? HeightReference.RELATIVE_TO_GROUND
: undefined),
disableDepthTestDistance: this.disableDepthTest
? new ConstantProperty(Number.POSITIVE_INFINITY)
: undefined
});
if (properties &&
isDefined(properties["marker-opacity"]) &&
entity.point.color) {
// not part of SimpleStyle spec, but why not?
const color = entity.point.color.getValue(now);
color.alpha = parseFloat(properties["marker-opacity"]?.getValue());
}
entity.billboard = undefined;
}
if (isDefined(entity.billboard) &&
properties &&
isDefined(properties["marker-opacity"]?.getValue())) {
entity.billboard.color = new ConstantProperty(new Color(1, 1, 1, parseFloat(properties["marker-opacity"]?.getValue())));
}
if (isDefined(entity.polygon)) {
// Extrude polygons if heightProperty is set
if (this.heightProperty &&
properties &&
isDefined(properties[this.heightProperty])) {
entity.polygon.closeTop = new ConstantProperty(true);
entity.polygon.extrudedHeight = properties[this.heightProperty];
entity.polygon.heightReference = new ConstantProperty(HeightReference.CLAMP_TO_GROUND);
entity.polygon.extrudedHeightReference = new ConstantProperty(HeightReference.RELATIVE_TO_GROUND);
}
// Cesium on Windows can't render polygons with a stroke-width > 1.0. And even on other platforms it
// looks bad because WebGL doesn't mitre the lines together nicely.
// As a workaround for the special case where the polygon is unfilled anyway, change it to a polyline.
else if (polygonHasWideOutline(entity.polygon, now) &&
!polygonIsFilled(entity.polygon)) {
createPolylineFromPolygon(entities, entity, now);
entity.polygon = undefined;
}
else if (polygonHasOutline(entity.polygon, now) &&
isPolygonOnTerrain(entity.polygon, now)) {
// Polygons don't directly support outlines when they're on terrain.
// So create a manual outline.
createPolylineFromPolygon(entities, entity, now);
}
}
}
return dataSource;
}
get discreteTimes() {
if (this.readyData === undefined) {
return undefined;
}
// If we are using mvt (mapbox vector tiles / protomaps imagery provider) return TableMixin.discreteTimes
if (this.useTableStylingAndProtomaps)
return super.discreteTimes;
// If using timeProperty - get discrete times from that
if (this.timeProperty) {
const discreteTimesMap = new Map();
for (let i = 0; i < this.readyData.features.length; i++) {
const feature = this.readyData.features[i];
if (feature.properties !== null &&
feature.properties !== undefined &&
feature.properties[this.timeProperty] !== undefined) {
const dt = {
time: new Date(`${feature.properties[this.timeProperty]}`).toISOString(),
tag: feature.properties[this.timeProperty]
};
discreteTimesMap.set(dt.tag, dt);
}
}
return Array.from(discreteTimesMap.values());
}
}
/**
* Transform feature properties into column-major format.
* This enables all TableMixin functionality - which is used for styling vector tiles.
* If this returns an empty array, TableMixin will effectively be disabled
*/
get dataColumnMajor() {
if (!this.readyData || !this.useTableStylingAndProtomaps)
return [];
// Map from property name (column name) to column index
const colMap = new Map();
const dataColumnMajor = [];
dataColumnMajor[0] = new Array(this.readyData.features.length + 1).fill("");
for (let i = 0; i < this.readyData.features.length; i++) {
const feature = this.readyData.features[i];
// Loop through feature properties
if (feature.properties) {
for (let j = 0; j < Object.keys(feature.properties).length; j++) {
const prop = Object.keys(feature.properties)[j];
const value = feature.properties[prop];
let colIndex = colMap.get(prop);
// If column isn't in colMap - we need to create it
if (!isDefined(colIndex)) {
colIndex = colMap.size;
colMap.set(prop, colIndex);
dataColumnMajor[colIndex] = new Array(this.readyData.features.length + 1).fill("");
}
if (typeof value === "string") {
dataColumnMajor[colIndex][i + 1] = value;
}
else if (typeof value === "number") {
dataColumnMajor[colIndex][i + 1] = value.toString();
}
}
}
}
// Set column titles
colMap.forEach((index, prop) => {
dataColumnMajor[index][0] = prop;
});
return dataColumnMajor;
}
/** We don't need to use TableMixin forceLoadTableData
* We implement `get dataColumnMajor()` instead
*/
async forceLoadTableData() {
return undefined;
}
get viewingControls() {
return !this.useTableStylingAndProtomaps
? super.viewingControls.filter((v) => v.id !== TableStylingWorkflow.type)
: super.viewingControls;
}
}
__decorate([
observable
], GeoJsonMixin.prototype, "_dataSource", void 0);
__decorate([
observable
], GeoJsonMixin.prototype, "_imageryProvider", void 0);
__decorate([
observable.ref
], GeoJsonMixin.prototype, "_readyData", void 0);
__decorate([
observable
], GeoJsonMixin.prototype, "featureCounts", void 0);
__decorate([
override
], GeoJsonMixin.prototype, "name", null);
__decorate([
override
], GeoJsonMixin.prototype, "cacheDuration", null);
__decorate([
computed
], GeoJsonMixin.prototype, "readyData", null);
__decorate([
override
], GeoJsonMixin.prototype, "_canExportData", null);
__decorate([
override
], GeoJsonMixin.prototype, "mapItems", null);
__decorate([
computed
], GeoJsonMixin.prototype, "useTableStylingAndProtomaps", null);
__decorate([
override
], GeoJsonMixin.prototype, "chartItems", null);
__decorate([
action
], GeoJsonMixin.prototype, "addPerPropertyStyleToGeoJson", null);
__decorate([
action
], GeoJsonMixin.prototype, "createProtomapsImageryProvider", null);
__decorate([
computed
], GeoJsonMixin.prototype, "defaultStyles", null);
__decorate([
computed
], GeoJsonMixin.prototype, "stylesWithDefaults", null);
__decorate([
override
], GeoJsonMixin.prototype, "discreteTimes", null);
__decorate([
override
], GeoJsonMixin.prototype, "dataColumnMajor", null);
__decorate([
override
], GeoJsonMixin.prototype, "viewingControls", null);
return GeoJsonMixin;
}
(function (GeoJsonMixin) {
function isMixedInto(model) {
return model && model.isGeoJson;
}
GeoJsonMixin.isMixedInto = isMixedInto;
})(GeoJsonMixin || (GeoJsonMixin = {}));
export default GeoJsonMixin;
function createPolylineFromPolygon(entities, entity, now) {
const polygon = entity.polygon;
entity.polyline = new PolylineGraphics();
entity.polyline.show = polygon.show;
if (isPolygonOnTerrain(polygon, now)) {
entity.polyline.clampToGround = new ConstantProperty(true);
}
if (isDefined(polygon.outlineColor)) {
entity.polyline.material = new ColorMaterialProperty(polygon.outlineColor);
}
const hierarchy = getPropertyValue(polygon.hierarchy);
if (!hierarchy) {
return;
}
const positions = closePolyline(hierarchy.positions);
entity.polyline.positions = new ConstantProperty(positions);
entity.polyline.width =
polygon.outlineWidth &&