terriajs
Version:
Geospatial data visualization platform.
418 lines • 18.7 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 { get as _get } from "lodash-es";
import { computed, makeObservable, observable, runInAction } from "mobx";
import { createTransformer } from "mobx-utils";
import Pbf from "pbf";
import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3";
import Color from "terriajs-cesium/Source/Core/Color";
import HeadingPitchRoll from "terriajs-cesium/Source/Core/HeadingPitchRoll";
import JulianDate from "terriajs-cesium/Source/Core/JulianDate";
import NearFarScalar from "terriajs-cesium/Source/Core/NearFarScalar";
import Transforms from "terriajs-cesium/Source/Core/Transforms";
import BillboardGraphics from "terriajs-cesium/Source/DataSources/BillboardGraphics";
import ConstantPositionProperty from "terriajs-cesium/Source/DataSources/ConstantPositionProperty";
import ConstantProperty from "terriajs-cesium/Source/DataSources/ConstantProperty";
import DataSource from "terriajs-cesium/Source/DataSources/CustomDataSource";
import ModelGraphics from "terriajs-cesium/Source/DataSources/ModelGraphics";
import PointGraphics from "terriajs-cesium/Source/DataSources/PointGraphics";
import PropertyBag from "terriajs-cesium/Source/DataSources/PropertyBag";
import Axis from "terriajs-cesium/Source/Scene/Axis";
import ColorBlendMode from "terriajs-cesium/Source/Scene/ColorBlendMode";
import HeightReference from "terriajs-cesium/Source/Scene/HeightReference";
import ShadowMode from "terriajs-cesium/Source/Scene/ShadowMode";
import isDefined from "../../../Core/isDefined";
import loadArrayBuffer from "../../../Core/loadArrayBuffer";
import TerriaError from "../../../Core/TerriaError";
import AutoRefreshingMixin from "../../../ModelMixins/AutoRefreshingMixin";
import CatalogMemberMixin from "../../../ModelMixins/CatalogMemberMixin";
import MappableMixin from "../../../ModelMixins/MappableMixin";
import UrlMixin from "../../../ModelMixins/UrlMixin";
import GtfsCatalogItemTraits from "../../../Traits/TraitsClasses/GtfsCatalogItemTraits";
import { RectangleTraits } from "../../../Traits/TraitsClasses/MappableTraits";
import CreateModel from "../../Definition/CreateModel";
import createStratumInstance from "../../Definition/createStratumInstance";
import LoadableStratum from "../../Definition/LoadableStratum";
import StratumOrder from "../../Definition/StratumOrder";
import proxyCatalogItemUrl from "../proxyCatalogItemUrl";
import { FeedMessageReader } from "./GtfsRealtimeProtoBufReaders";
import prettyPrintGtfsEntityField from "./prettyPrintGtfsEntityField";
class GtfsStratum extends LoadableStratum(GtfsCatalogItemTraits) {
_item;
static stratumName = "gtfs";
constructor(_item) {
super();
this._item = _item;
makeObservable(this);
}
duplicateLoadableStratum(newModel) {
return new GtfsStratum(newModel);
}
static async load(item) {
return new GtfsStratum(item);
}
get rectangle() {
return createStratumInstance(RectangleTraits, this._item._bbox);
}
}
__decorate([
computed
], GtfsStratum.prototype, "rectangle", null);
StratumOrder.addLoadStratum(GtfsStratum.stratumName);
/**
* For displaying realtime transport data. See [here](https://developers.google.com/transit/gtfs-realtime/reference/)
* for the spec.
*/
export default class GtfsCatalogItem extends UrlMixin(AutoRefreshingMixin(MappableMixin(CatalogMemberMixin(CreateModel(GtfsCatalogItemTraits))))) {
disposer;
_bbox = {
west: Infinity,
south: Infinity,
east: -Infinity,
north: -Infinity
};
/**
* Always use the getter to read this. This is a cache for a computed property.
*
* We cache it because recreating it reactively is computationally expensive, so we modify it reactively instead.
*/
_dataSource = new DataSource("billboard");
static FEATURE_INFO_TEMPLATE_FIELDS = [
"route_short_name",
"occupancy_status#str",
"speed#km",
"speed",
"bearing"
];
static get type() {
return "gtfs";
}
get type() {
return GtfsCatalogItem.type;
}
gtfsFeedEntities = [];
convertManyFeedEntitiesToBillboardData = createTransformer((feedEntities) => {
// Sometimes the feed can contain many records for the same vehicle
// so we'll only display the newest record.
// Although technically the timestamp property is optional, if none is
// present we'll show the record.
const vehicleMap = new Map();
for (let i = 0; i < feedEntities.length; ++i) {
const entity = feedEntities[i];
const item = this.convertFeedEntityToBillboardData(entity);
if (item && item.position && item.featureInfo) {
const vehicleInfo = item.featureInfo.get("entity").vehicle.vehicle;
if (vehicleMap.has(vehicleInfo.id) && vehicleInfo.timestamp) {
const existingRecord = vehicleMap.get(vehicleInfo.id);
if (existingRecord.timestamp < vehicleInfo.timestamp) {
vehicleMap.set(vehicleInfo.id, item);
}
}
else {
vehicleMap.set(vehicleInfo.id, item);
}
}
}
return [...vehicleMap.values()];
});
get dataSource() {
this._dataSource.entities.suspendEvents();
// Convert the GTFS protobuf into a more useful shape
const vehicleData = this.convertManyFeedEntitiesToBillboardData(this.gtfsFeedEntities);
for (const data of vehicleData) {
if (data.sourceId === undefined) {
continue;
}
const entity = this._dataSource.entities.getOrCreateEntity(data.sourceId);
if (!entity.model) {
if (this._coloredModels) {
const gtfsEntity = data.featureInfo?.get("entity");
const value = _get(gtfsEntity, this.model.colorModelsByProperty.property);
if (value !== undefined) {
const index = this.model.colorModelsByProperty.colorGroups.findIndex((colorGroup) => colorGroup.regExp !== undefined &&
new RegExp(colorGroup.regExp).test(value));
if (index !== -1) {
entity.model = this._coloredModels[index];
}
entity.point = undefined;
}
else {
entity.model = this._model;
}
}
else if (this._model) {
entity.model = this._model;
}
}
if (this.model !== undefined &&
this.model !== null &&
data.orientation !== undefined &&
data.orientation !== null) {
entity.orientation = new ConstantProperty(data.orientation);
}
if (data.position !== undefined &&
(!entity.position ||
entity.position.getValue(new JulianDate()) !== data.position)) {
entity.position = new ConstantPositionProperty(data.position);
}
// If we're using a billboard
if (data.billboard !== null && data.billboard !== undefined) {
if (entity.billboard === null || entity.billboard === undefined) {
entity.billboard = data.billboard;
}
if (data.billboard.color) {
data.billboard.color.getValue(new JulianDate()).alpha = this.opacity;
}
if (!entity.billboard.color ||
!entity.billboard.color.equals(data.billboard.color)) {
entity.billboard.color = data.billboard.color;
}
}
// If we're using a point
if (data.point !== null && data.point !== undefined) {
if (entity.point === null || entity.point === undefined) {
entity.point = data.point;
}
if (data.point.color) {
data.point.color.getValue(new JulianDate()).alpha = this.opacity;
}
if (!entity.point.color ||
!entity.point.color.equals(data.point.color)) {
entity.point.color = data.point.color;
}
}
if (data.featureInfo !== undefined && data.featureInfo !== null) {
entity.properties = new PropertyBag();
for (const key of data.featureInfo.keys()) {
entity.properties.addProperty(key, data.featureInfo.get(key));
}
}
}
// remove entities that no longer exist
if (this._dataSource.entities.values.length > vehicleData.length) {
const idSet = new Set(vehicleData.map((val) => val.sourceId));
this._dataSource.entities.values
.filter((entity) => !idSet.has(entity.id))
.forEach((entity) => this._dataSource.entities.remove(entity));
}
this._dataSource.entities.resumeEvents();
return this._dataSource;
}
refreshData() {
this.forceLoadMapItems();
}
get mapItems() {
this._dataSource.show = this.show;
return [this.dataSource];
}
get _cesiumUpAxis() {
if (this.model.upAxis === undefined) {
return Axis.Y;
}
return Axis.fromName(this.model.upAxis);
}
get _cesiumForwardAxis() {
if (this.model.forwardAxis === undefined) {
return Axis.Z;
}
return Axis.fromName(this.model.forwardAxis);
}
get _model() {
if (this.model.url === undefined) {
return undefined;
}
const options = {
uri: new ConstantProperty(this.model.url),
upAxis: new ConstantProperty(this._cesiumUpAxis),
forwardAxis: new ConstantProperty(this._cesiumForwardAxis),
scale: new ConstantProperty(this.model.scale ?? 1),
heightReference: new ConstantProperty(HeightReference.RELATIVE_TO_GROUND),
distanceDisplayCondition: new ConstantProperty({
near: 0.0,
far: this.model.maximumDistance
}),
maximumScale: new ConstantProperty(this.model.maximumScale),
minimumPixelSize: new ConstantProperty(this.model.minimumPixelSize ?? 0),
shadows: ShadowMode.DISABLED
};
return new ModelGraphics(options);
}
get _coloredModels() {
const colorGroups = this.model?.colorModelsByProperty?.colorGroups;
const model = this._model;
if (!isDefined(model) ||
!isDefined(this.model?.colorModelsByProperty?.property) ||
!isDefined(colorGroups) ||
colorGroups.length === 0) {
return undefined;
}
return colorGroups.map(({ color }) => {
const coloredModel = model.clone();
coloredModel.color = new ConstantProperty(Color.fromCssColorString(color ?? "white"));
coloredModel.colorBlendMode = new ConstantProperty(ColorBlendMode.MIX);
coloredModel.colorBlendAmount = new ConstantProperty(0.7);
return coloredModel;
});
}
constructor(id, terria, sourceReference) {
super(id, terria, sourceReference);
makeObservable(this);
}
forceLoadMetadata() {
return Promise.resolve();
}
forceLoadMapItems() {
if (this.strata.get(GtfsStratum.stratumName) === undefined) {
GtfsStratum.load(this).then((stratum) => {
runInAction(() => {
this.strata.set(GtfsStratum.stratumName, stratum);
});
});
}
const promise = this.retrieveData()
.then((data) => {
runInAction(() => {
if (data.entity !== undefined && data.entity !== null) {
this.gtfsFeedEntities = data.entity;
this.terria.currentViewer.notifyRepaintRequired();
}
});
})
.catch((_e) => {
throw new TerriaError({
title: `Could not load ${this.nameInCatalog}.`,
sender: this,
message: `There was an error loading the data for ${this.nameInCatalog}.`
});
});
return promise;
}
retrieveData() {
// These headers work for the Transport for NSW APIs. Presumably, other services will require different headers.
const headers = {
"Content-Type": "application/x-google-protobuf;charset=UTF-8",
"Cache-Control": "no-cache"
};
if (this.headers !== undefined) {
this.headers.forEach(({ name, value }) => {
if (name !== undefined && value !== undefined)
headers[name] = value;
});
}
if (this.url !== null && this.url !== undefined) {
return loadArrayBuffer(proxyCatalogItemUrl(this, this.url), headers).then((arr) => {
const pbfBuffer = new Pbf(new Uint8Array(arr));
return new FeedMessageReader().read(pbfBuffer);
});
}
else {
return Promise.reject();
}
}
convertFeedEntityToBillboardData(entity) {
if (entity.id === undefined) {
return {};
}
let position = undefined;
let orientation = undefined;
const featureInfo = new Map();
if (entity.vehicle !== null &&
entity.vehicle !== undefined &&
entity.vehicle.position !== null &&
entity.vehicle.position !== undefined &&
entity.vehicle.position.latitude !== null &&
entity.vehicle.position.latitude !== undefined &&
entity.vehicle.position.longitude !== null &&
entity.vehicle.position.longitude !== undefined &&
entity.vehicle.position.bearing !== null &&
entity.vehicle.position.bearing !== undefined) {
updateBbox(entity.vehicle.position.latitude, entity.vehicle.position.longitude, this._bbox);
position = Cartesian3.fromDegrees(entity.vehicle.position.longitude, entity.vehicle.position.latitude);
orientation = Transforms.headingPitchRollQuaternion(position, HeadingPitchRoll.fromDegrees(entity.vehicle.position.bearing - 90.0, 0.0, 0.0));
}
// Add the values that the feature info template gets populated with
for (const field of GtfsCatalogItem.FEATURE_INFO_TEMPLATE_FIELDS) {
featureInfo.set(field, prettyPrintGtfsEntityField(field, entity));
}
featureInfo.set("entity", entity);
let billboard;
let point;
if (this.image !== undefined && this.image !== null) {
billboard = new BillboardGraphics({
image: new ConstantProperty(this.image),
heightReference: new ConstantProperty(HeightReference.RELATIVE_TO_GROUND),
scaleByDistance: this.scaleImageByDistance.nearValue ===
this.scaleImageByDistance.farValue
? undefined
: new ConstantProperty(new NearFarScalar(this.scaleImageByDistance.near, this.scaleImageByDistance.nearValue, this.scaleImageByDistance.far, this.scaleImageByDistance.farValue)),
scale: this.scaleImageByDistance.nearValue ===
this.scaleImageByDistance.farValue &&
this.scaleImageByDistance.nearValue !== 1.0
? new ConstantProperty(this.scaleImageByDistance.nearValue)
: undefined,
color: new ConstantProperty(new Color(1.0, 1.0, 1.0, this.opacity))
});
}
else {
point = new PointGraphics({
color: new ConstantProperty(Color.CYAN),
outlineWidth: new ConstantProperty(1),
outlineColor: new ConstantProperty(Color.WHITE),
scaleByDistance: this.scaleImageByDistance.nearValue ===
this.scaleImageByDistance.farValue
? undefined
: new ConstantProperty(new NearFarScalar(this.scaleImageByDistance.near, this.scaleImageByDistance.nearValue, this.scaleImageByDistance.far, this.scaleImageByDistance.farValue)),
pixelSize: this.scaleImageByDistance.nearValue ===
this.scaleImageByDistance.farValue &&
this.scaleImageByDistance.nearValue !== 1.0
? new ConstantProperty(32 * this.scaleImageByDistance.nearValue)
: new ConstantProperty(32),
heightReference: new ConstantProperty(HeightReference.RELATIVE_TO_GROUND)
});
}
return {
sourceId: entity.id,
position: position,
orientation: orientation,
featureInfo: featureInfo,
billboard: billboard,
point: point
};
}
}
__decorate([
observable
], GtfsCatalogItem.prototype, "gtfsFeedEntities", void 0);
__decorate([
computed
], GtfsCatalogItem.prototype, "dataSource", null);
__decorate([
computed
], GtfsCatalogItem.prototype, "mapItems", null);
__decorate([
computed
], GtfsCatalogItem.prototype, "_cesiumUpAxis", null);
__decorate([
computed
], GtfsCatalogItem.prototype, "_cesiumForwardAxis", null);
__decorate([
computed
], GtfsCatalogItem.prototype, "_model", null);
__decorate([
computed
], GtfsCatalogItem.prototype, "_coloredModels", null);
function updateBbox(lat, lon, rectangle) {
if (lon < rectangle.west)
rectangle.west = lon;
if (lat < rectangle.south)
rectangle.south = lat;
if (lon > rectangle.east)
rectangle.east = lon;
if (lat > rectangle.north)
rectangle.north = lat;
}
//# sourceMappingURL=GtfsCatalogItem.js.map