terriajs
Version:
Geospatial data visualization platform.
1,353 lines (1,198 loc) • 38.2 kB
text/typescript
import AssociativeArray from "terriajs-cesium/Source/Core/AssociativeArray";
import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2";
import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3";
import CesiumMath from "terriajs-cesium/Source/Core/Math";
import Color from "terriajs-cesium/Source/Core/Color";
import DataSource from "terriajs-cesium/Source/DataSources/DataSource";
import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid";
import Entity from "terriajs-cesium/Source/DataSources/Entity";
import EntityCollection from "terriajs-cesium/Source/DataSources/EntityCollection";
import EntityCluster from "terriajs-cesium/Source/DataSources/EntityCluster";
import isDefined from "../../Core/isDefined";
import JulianDate from "terriajs-cesium/Source/Core/JulianDate";
import L, { LatLngBounds, PolylineOptions, LatLngBoundsLiteral } from "leaflet";
import LeafletScene from "./LeafletScene";
import PolygonHierarchy from "terriajs-cesium/Source/Core/PolygonHierarchy";
import PolylineGlowMaterialProperty from "terriajs-cesium/Source/DataSources/PolylineGlowMaterialProperty";
import PolylineDashMaterialProperty from "terriajs-cesium/Source/DataSources/PolylineDashMaterialProperty";
import Property from "terriajs-cesium/Source/DataSources/Property";
import Rectangle from "terriajs-cesium/Source/Core/Rectangle";
import { getLineStyleLeaflet } from "../../Models/Catalog/Esri/esriLineStyle";
const destroyObject =
require("terriajs-cesium/Source/Core/destroyObject").default;
const writeTextToCanvas =
require("terriajs-cesium/Source/Core/writeTextToCanvas").default;
interface PointDetails {
layer?: L.CircleMarker;
lastPosition: Cartesian3;
lastPixelSize: number;
lastColor: Color;
lastOutlineColor: Color;
lastOutlineWidth: number;
}
interface PolygonDetails {
layer?: L.Polygon;
lastHierarchy?: PolygonHierarchy;
lastFill?: boolean;
lastFillColor: Color;
lastOutline?: boolean;
lastOutlineColor: Color;
}
interface RectangleDetails {
layer?: L.Rectangle;
lastFill?: boolean;
lastFillColor: Color;
lastOutline?: boolean;
lastOutlineColor: Color;
}
interface BillboardDetails {
layer?: L.Marker;
}
interface LabelDetails {
layer?: L.Marker;
}
interface PolylineDetails {
layer?: L.Polyline;
}
interface EntityDetails {
point?: PointDetails;
polygon?: PolygonDetails;
billboard?: BillboardDetails;
label?: LabelDetails;
polyline?: PolylineDetails;
rectangle?: RectangleDetails;
}
interface EntityHash {
[key: string]: EntityDetails;
}
const defaultColor = Color.WHITE;
const defaultOutlineColor = Color.BLACK;
const defaultOutlineWidth = 1.0;
const defaultPixelSize = 5.0;
const defaultWidth = 5.0;
//Single pixel black dot
const tmpImage =
"data:image/gif;base64,R0lGODlhAQABAPAAAAAAAP///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
//NOT IMPLEMENTED
// Path primitive - no need identified
// Ellipse primitive - no need identified
// Ellipsoid primitive - 3d prim - no plans for this
// Model primitive - 3d prim - no plans for this
/**
* A variable to store what sort of bounds our leaflet map has been looking at
* 0 = a normal extent
* 1 = zoomed in close to the east/left of anti-meridian
* 2 = zoomed in close to the west/right of the anti-meridian
* When this value changes we'll need to recompute the location of our points
* to help them wrap around the anti-meridian
*/
let prevBoundsType = 0;
/**
* A {@link Visualizer} which maps {@link Entity#point} to Leaflet primitives.
**/
class LeafletGeomVisualizer {
private readonly _featureGroup: L.FeatureGroup;
private readonly _entitiesToVisualize: AssociativeArray;
private readonly _entityHash: EntityHash;
constructor(
readonly leafletScene: LeafletScene,
readonly entityCollection: EntityCollection
) {
entityCollection.collectionChanged.addEventListener(
this._onCollectionChanged,
this
);
this._featureGroup = L.featureGroup().addTo(leafletScene.map);
this._entitiesToVisualize = new AssociativeArray();
this._entityHash = {};
this._onCollectionChanged(
entityCollection,
entityCollection.values,
[],
[]
);
}
private _onCollectionChanged(
_entityCollection: EntityCollection,
added: Entity[],
removed: Entity[],
changed: Entity[]
) {
let entity;
const featureGroup = this._featureGroup;
const entities = this._entitiesToVisualize;
const entityHash = this._entityHash;
for (let i = added.length - 1; i > -1; i--) {
entity = added[i];
if (
((isDefined(entity.point) ||
isDefined(entity.billboard) ||
isDefined(entity.label)) &&
isDefined(entity.position)) ||
isDefined(entity.polyline) ||
isDefined(entity.polygon) ||
isDefined(entity.rectangle)
) {
entities.set(entity.id, entity);
entityHash[entity.id] = {};
}
}
for (let i = changed.length - 1; i > -1; i--) {
entity = changed[i];
if (
((isDefined(entity.point) ||
isDefined(entity.billboard) ||
isDefined(entity.label)) &&
isDefined(entity.position)) ||
isDefined(entity.polyline) ||
isDefined(entity.polygon) ||
isDefined(entity.rectangle)
) {
entities.set(entity.id, entity);
entityHash[entity.id] = entityHash[entity.id] || {};
} else {
cleanEntity(entity, featureGroup, entityHash);
entities.remove(entity.id);
}
}
for (let i = removed.length - 1; i > -1; i--) {
entity = removed[i];
cleanEntity(entity, featureGroup, entityHash);
entities.remove(entity.id);
}
}
/**
* Updates the primitives created by this visualizer to match their
* Entity counterpart at the given time.
*
*/
public update(time: JulianDate): boolean {
const entities = this._entitiesToVisualize.values;
const entityHash = this._entityHash;
const bounds = this.leafletScene.map.getBounds();
let applyLocalisedAntiMeridianFix = false;
let currentBoundsType = 0;
if (_isCloseToEasternAntiMeridian(bounds)) {
applyLocalisedAntiMeridianFix = true;
currentBoundsType = 1;
} else if (_isCloseToWesternAntiMeridian(bounds)) {
applyLocalisedAntiMeridianFix = true;
currentBoundsType = 2;
}
for (let i = 0, len = entities.length; i < len; i++) {
const entity = entities[i];
const entityDetails = entityHash[entity.id];
if (isDefined(entity._point)) {
this._updatePoint(
entity,
time,
entityHash,
entityDetails,
applyLocalisedAntiMeridianFix === true ? bounds : undefined,
prevBoundsType !== currentBoundsType
);
}
if (isDefined(entity.billboard)) {
this._updateBillboard(
entity,
time,
entityHash,
entityDetails,
applyLocalisedAntiMeridianFix === true ? bounds : undefined
);
}
if (isDefined(entity.label)) {
this._updateLabel(entity, time, entityHash, entityDetails);
}
if (isDefined(entity.polyline)) {
this._updatePolyline(entity, time, entityHash, entityDetails);
}
if (isDefined(entity.polygon)) {
this._updatePolygon(entity, time, entityHash, entityDetails);
}
if (isDefined(entity.rectangle)) {
this._updateRectangle(entity, time, entityHash, entityDetails);
}
}
prevBoundsType = currentBoundsType;
return true;
}
private _updatePoint(
entity: Entity,
time: JulianDate,
_entityHash: EntityHash,
entityDetails: EntityDetails,
bounds: LatLngBounds | undefined,
boundsJustChanged: boolean
) {
const featureGroup = this._featureGroup;
const pointGraphics = entity.point!;
const show =
entity.isAvailable(time) &&
getValueOrDefault(pointGraphics.show, time, true);
if (!show) {
cleanPoint(entity, featureGroup, entityDetails);
return;
}
let details = entityDetails.point;
if (!isDefined(details)) {
details = entityDetails.point = {
layer: undefined,
lastPosition: new Cartesian3(),
lastPixelSize: 1,
lastColor: new Color(),
lastOutlineColor: new Color(),
lastOutlineWidth: 1
};
}
const position = getValueOrUndefined(entity.position, time);
if (!isDefined(position)) {
cleanPoint(entity, featureGroup, entityDetails);
return;
}
const pixelSize = getValueOrDefault(
pointGraphics.pixelSize,
time,
defaultPixelSize
);
const color = getValueOrDefault(pointGraphics.color, time, defaultColor);
const outlineColor = getValueOrDefault(
pointGraphics.outlineColor,
time,
defaultOutlineColor
);
const outlineWidth = getValueOrDefault(
pointGraphics.outlineWidth,
time,
defaultOutlineWidth
);
let layer = details.layer;
if (!isDefined(layer)) {
const pointOptions = {
radius: pixelSize / 2.0,
fillColor: color.toCssColorString(),
fillOpacity: color.alpha,
color: outlineColor.toCssColorString(),
weight: outlineWidth,
opacity: outlineColor.alpha
};
layer = details.layer = L.circleMarker(
positionToLatLng(position, bounds),
pointOptions
);
layer.on("click", featureClicked.bind(undefined, this, entity));
layer.on("mousedown", featureMousedown.bind(undefined, this, entity));
featureGroup.addLayer(layer);
Cartesian3.clone(position, details.lastPosition);
details.lastPixelSize = pixelSize;
Color.clone(color, details.lastColor);
Color.clone(outlineColor, details.lastOutlineColor);
details.lastOutlineWidth = outlineWidth;
return layer;
}
if (
!Cartesian3.equals(position, details.lastPosition) ||
boundsJustChanged
) {
layer.setLatLng(positionToLatLng(position, bounds));
Cartesian3.clone(position, details.lastPosition);
}
if (pixelSize !== details.lastPixelSize) {
layer.setRadius(pixelSize / 2.0);
details.lastPixelSize = pixelSize;
}
const options = layer.options;
let applyStyle = false;
if (!Color.equals(color, details.lastColor)) {
options.fillColor = color.toCssColorString();
options.fillOpacity = color.alpha;
Color.clone(color, details.lastColor);
applyStyle = true;
}
if (!Color.equals(outlineColor, details.lastOutlineColor)) {
options.color = outlineColor.toCssColorString();
options.opacity = outlineColor.alpha;
Color.clone(outlineColor, details.lastOutlineColor);
applyStyle = true;
}
if (outlineWidth !== details.lastOutlineWidth) {
options.weight = outlineWidth;
details.lastOutlineWidth = outlineWidth;
applyStyle = true;
}
if (applyStyle) {
layer.setStyle(options);
}
}
private _updateBillboard(
entity: Entity,
time: JulianDate,
_entityHash: EntityHash,
entityDetails: EntityDetails,
bounds: LatLngBounds | undefined
) {
const markerGraphics = entity.billboard!;
const featureGroup = this._featureGroup;
let position;
let marker: L.Marker;
let details = entityDetails.billboard;
if (!isDefined(details)) {
details = entityDetails.billboard = {
layer: undefined
};
}
const geomLayer = details.layer;
let show =
entity.isAvailable(time) &&
getValueOrDefault(markerGraphics.show, time, true);
if (show) {
position = getValueOrUndefined(entity.position, time);
show = isDefined(position);
}
if (!show) {
cleanBillboard(entity, featureGroup, entityDetails);
return;
}
const latlng = positionToLatLng(position, bounds);
const image: any = getValue(markerGraphics.image, time);
const height: number | undefined = getValue(markerGraphics.height, time);
const width: number | undefined = getValue(markerGraphics.width, time);
const color = getValueOrDefault(markerGraphics.color, time, defaultColor);
const scale = getValueOrDefault(markerGraphics.scale, time, 1.0);
const verticalOrigin = getValueOrDefault(
markerGraphics.verticalOrigin,
time,
0
);
const horizontalOrigin = getValueOrDefault(
markerGraphics.horizontalOrigin,
time,
0
);
const pixelOffset = getValueOrDefault(
markerGraphics.pixelOffset,
time,
Cartesian2.ZERO
);
let imageUrl: string | undefined;
if (isDefined(image)) {
if (typeof image === "string") {
imageUrl = image;
} else if (isDefined(image.toDataURL)) {
imageUrl = image.toDataURL();
} else if (isDefined(image.url)) {
imageUrl = image.url;
} else {
imageUrl = image.src;
}
}
const iconOptions: any = {
color: color.toCssColorString(),
origUrl: imageUrl,
scale: scale,
horizontalOrigin: horizontalOrigin, //value: left, center, right
verticalOrigin: verticalOrigin //value: bottom, center, top
};
if (isDefined(height) || isDefined(width)) {
iconOptions.iconSize = [width, height];
}
let redrawIcon = false;
if (!isDefined(geomLayer)) {
const markerOptions = { icon: L.icon({ iconUrl: tmpImage }) };
marker = L.marker(latlng, markerOptions);
marker.on("click", featureClicked.bind(undefined, this, entity));
marker.on("mousedown", featureMousedown.bind(undefined, this, entity));
featureGroup.addLayer(marker);
details.layer = marker;
redrawIcon = true;
} else {
marker = geomLayer;
if (!marker.getLatLng().equals(latlng)) {
marker.setLatLng(latlng);
}
for (const prop in iconOptions) {
if (
isDefined(marker.options.icon) &&
iconOptions[prop] !== (<any>marker.options.icon.options)[prop]
) {
redrawIcon = true;
break;
}
}
}
if (redrawIcon) {
const recolorNeeded = !color.equals(defaultColor);
const drawBillboard = function (
image: HTMLImageElement,
dataurl: string | undefined
) {
iconOptions.iconUrl = dataurl || image;
if (!isDefined(iconOptions.iconSize)) {
iconOptions.iconSize = [image.width * scale, image.height * scale];
}
const w = iconOptions.iconSize[0],
h = iconOptions.iconSize[1];
const xOff = (w / 2) * (1 - horizontalOrigin) - pixelOffset.x;
const yOff = (h / 2) * (1 + verticalOrigin) - pixelOffset.y;
iconOptions.iconAnchor = [xOff, yOff];
if (recolorNeeded) {
iconOptions.iconUrl = recolorBillboard(image, color);
}
marker.setIcon(L.icon(iconOptions));
};
const img = new Image();
img.onload = function () {
drawBillboard(img, imageUrl);
};
if (isDefined(imageUrl)) {
img.src = imageUrl;
if (recolorNeeded) {
img.crossOrigin = "anonymous";
}
}
}
}
private _updateLabel(
entity: Entity,
time: JulianDate,
_entityHash: EntityHash,
entityDetails: EntityDetails
) {
const labelGraphics = entity.label!;
const featureGroup = this._featureGroup;
let position;
let marker: L.Marker;
let details = entityDetails.label;
if (!isDefined(details)) {
details = entityDetails.label = {
layer: undefined
};
}
const geomLayer = details.layer;
let show =
entity.isAvailable(time) &&
getValueOrDefault(labelGraphics.show, time, true);
if (show) {
position = getValueOrUndefined(entity.position, time);
show = isDefined(position);
}
if (!show) {
cleanLabel(entity, featureGroup, entityDetails);
return;
}
const cart = Ellipsoid.WGS84.cartesianToCartographic(position);
const latlng = L.latLng(
CesiumMath.toDegrees(cart.latitude),
CesiumMath.toDegrees(cart.longitude)
);
const text = getValue(labelGraphics.text, time);
const font = getValue(labelGraphics.font as unknown as Property, time);
const scale = getValueOrDefault(labelGraphics.scale, time, 1.0);
const fillColor = getValueOrDefault(
labelGraphics.fillColor as unknown as Property,
time,
defaultColor
);
const verticalOrigin = getValueOrDefault(
labelGraphics.verticalOrigin,
time,
0
);
const horizontalOrigin = getValueOrDefault(
labelGraphics.horizontalOrigin,
time,
0
);
const pixelOffset = getValueOrDefault(
labelGraphics.pixelOffset,
time,
Cartesian2.ZERO
);
const iconOptions: any = {
text: text,
font: font,
color: fillColor.toCssColorString(),
scale: scale,
horizontalOrigin: horizontalOrigin, //value: left, center, right
verticalOrigin: verticalOrigin //value: bottom, center, top
};
let redrawLabel = false;
if (!isDefined(geomLayer)) {
const markerOptions = { icon: L.icon({ iconUrl: tmpImage }) };
marker = L.marker(latlng, markerOptions);
marker.on("click", featureClicked.bind(undefined, this, entity));
marker.on("mousedown", featureMousedown.bind(undefined, this, entity));
featureGroup.addLayer(marker);
details.layer = marker;
redrawLabel = true;
} else {
marker = geomLayer;
if (!marker.getLatLng().equals(latlng)) {
marker.setLatLng(latlng);
}
for (const prop in iconOptions) {
if (
isDefined(marker.options.icon) &&
iconOptions[prop] !== (<any>marker.options.icon.options)[prop]
) {
redrawLabel = true;
break;
}
}
}
if (redrawLabel) {
const drawBillboard = function (
image: HTMLImageElement,
dataurl: string
) {
iconOptions.iconUrl = dataurl || image;
if (!isDefined(iconOptions.iconSize)) {
iconOptions.iconSize = [image.width * scale, image.height * scale];
}
const w = iconOptions.iconSize[0],
h = iconOptions.iconSize[1];
const xOff = (w / 2) * (1 - horizontalOrigin) - pixelOffset.x;
const yOff = (h / 2) * (1 + verticalOrigin) - pixelOffset.y;
iconOptions.iconAnchor = [xOff, yOff];
marker.setIcon(L.icon(iconOptions));
};
const canvas = writeTextToCanvas(text, {
fillColor: fillColor,
font: font
});
const imageUrl = canvas.toDataURL();
const img = new Image();
img.onload = function () {
drawBillboard(img, imageUrl);
};
img.src = imageUrl;
}
}
private _updateRectangle(
entity: Entity,
time: JulianDate,
_entityHash: EntityHash,
entityDetails: EntityDetails
) {
const featureGroup = this._featureGroup;
const rectangleGraphics = entity.rectangle;
if (!isDefined(rectangleGraphics)) {
return;
}
const show =
entity.isAvailable(time) &&
getValueOrDefault(rectangleGraphics.show, time, true);
const rectangleCoordinates = rectangleGraphics.coordinates?.getValue(
time
) as Rectangle;
if (!show || !isDefined(rectangleCoordinates)) {
cleanRectangle(entity, featureGroup, entityDetails);
return;
}
const rectangleBounds: LatLngBoundsLiteral = [
[
CesiumMath.toDegrees(rectangleCoordinates.south),
CesiumMath.toDegrees(rectangleCoordinates.west)
],
[
CesiumMath.toDegrees(rectangleCoordinates.north),
CesiumMath.toDegrees(rectangleCoordinates.east)
]
];
let details = entityDetails.rectangle;
if (!isDefined(details)) {
details = entityDetails.rectangle = {
layer: undefined,
lastFill: undefined,
lastFillColor: new Color(),
lastOutline: undefined,
lastOutlineColor: new Color()
};
}
const fill = getValueOrDefault(
rectangleGraphics.fill as unknown as Property,
time,
true
);
const outline = getValueOrDefault(rectangleGraphics.outline, time, true);
let dashArray;
if (rectangleGraphics.outline instanceof PolylineDashMaterialProperty) {
dashArray = getDashArray(rectangleGraphics.outline, time);
}
const outlineWidth = getValueOrDefault(
rectangleGraphics.outlineWidth as unknown as Property,
time,
defaultOutlineWidth
);
const outlineColor = getValueOrDefault(
rectangleGraphics.outlineColor as unknown as Property,
time,
defaultOutlineColor
);
const material = getValueOrUndefined(
rectangleGraphics.material as unknown as Property,
time
);
let fillColor;
if (isDefined(material) && isDefined(material.color)) {
fillColor = material.color;
} else {
fillColor = defaultColor;
}
let layer = details.layer;
if (!isDefined(layer)) {
const polygonOptions: PolylineOptions = {
fill: fill,
fillColor: fillColor.toCssColorString(),
fillOpacity: fillColor.alpha,
weight: outline ? outlineWidth : 0.0,
color: outlineColor.toCssColorString(),
opacity: outlineColor.alpha
};
if (outline && dashArray) {
polygonOptions.dashArray = dashArray
.map((x) => x * outlineWidth)
.join(",");
}
layer = details.layer = L.rectangle(rectangleBounds, polygonOptions);
layer.on("click", featureClicked.bind(undefined, this, entity));
layer.on("mousedown", featureMousedown.bind(undefined, this, entity));
featureGroup.addLayer(layer);
details.lastFill = fill;
details.lastOutline = outline;
Color.clone(fillColor, details.lastFillColor);
Color.clone(outlineColor, details.lastOutlineColor);
return;
}
const options = layer.options;
let applyStyle = false;
if (fill !== details.lastFill) {
options.fill = fill;
details.lastFill = fill;
applyStyle = true;
}
if (outline !== details.lastOutline) {
options.weight = outline ? outlineWidth : 0.0;
details.lastOutline = outline;
applyStyle = true;
}
if (!Color.equals(fillColor, details.lastFillColor)) {
options.fillColor = fillColor.toCssColorString();
options.fillOpacity = fillColor.alpha;
Color.clone(fillColor, details.lastFillColor);
applyStyle = true;
}
if (!Color.equals(outlineColor, details.lastOutlineColor)) {
options.color = outlineColor.toCssColorString();
options.opacity = outlineColor.alpha;
Color.clone(outlineColor, details.lastOutlineColor);
applyStyle = true;
}
if (!layer.getBounds().equals(rectangleBounds)) {
layer.setBounds(rectangleBounds);
}
if (applyStyle) {
layer.setStyle(options);
}
}
private _updatePolygon(
entity: Entity,
time: JulianDate,
_entityHash: EntityHash,
entityDetails: EntityDetails
) {
const featureGroup = this._featureGroup;
const polygonGraphics = entity.polygon!;
const show =
entity.isAvailable(time) &&
getValueOrDefault(polygonGraphics.show, time, true);
if (!show) {
cleanPolygon(entity, featureGroup, entityDetails);
return;
}
let details = entityDetails.polygon;
if (!isDefined(details)) {
details = entityDetails.polygon = {
layer: undefined,
lastHierarchy: undefined,
lastFill: undefined,
lastFillColor: new Color(),
lastOutline: undefined,
lastOutlineColor: new Color()
};
}
const hierarchy = getValueOrUndefined(polygonGraphics.hierarchy, time);
if (!isDefined(hierarchy)) {
cleanPolygon(entity, featureGroup, entityDetails);
return;
}
const fill = getValueOrDefault(
polygonGraphics.fill as unknown as Property,
time,
true
);
const outline = getValueOrDefault(polygonGraphics.outline, time, true);
let dashArray;
if (polygonGraphics.outline instanceof PolylineDashMaterialProperty) {
dashArray = getDashArray(polygonGraphics.outline, time);
}
const outlineWidth = getValueOrDefault(
polygonGraphics.outlineWidth as unknown as Property,
time,
defaultOutlineWidth
);
const outlineColor = getValueOrDefault(
polygonGraphics.outlineColor as unknown as Property,
time,
defaultOutlineColor
);
const material = getValueOrUndefined(
polygonGraphics.material as unknown as Property,
time
);
let fillColor;
if (isDefined(material) && isDefined(material.color)) {
fillColor = material.color;
} else {
fillColor = defaultColor;
}
let layer = details.layer;
if (!isDefined(layer)) {
const polygonOptions: PolylineOptions = {
fill: fill,
fillColor: fillColor.toCssColorString(),
fillOpacity: fillColor.alpha,
weight: outline ? outlineWidth : 0.0,
color: outlineColor.toCssColorString(),
opacity: outlineColor.alpha
};
if (outline && dashArray) {
polygonOptions.dashArray = dashArray
.map((x) => x * outlineWidth)
.join(",");
}
layer = details.layer = L.polygon(
hierarchyToLatLngs(hierarchy),
polygonOptions
);
layer.on("click", featureClicked.bind(undefined, this, entity));
layer.on("mousedown", featureMousedown.bind(undefined, this, entity));
featureGroup.addLayer(layer);
details.lastHierarchy = hierarchy;
details.lastFill = fill;
details.lastOutline = outline;
Color.clone(fillColor, details.lastFillColor);
Color.clone(outlineColor, details.lastOutlineColor);
return;
}
if (hierarchy !== details.lastHierarchy) {
layer.setLatLngs(hierarchyToLatLngs(hierarchy));
details.lastHierarchy = hierarchy;
}
const options = layer.options;
let applyStyle = false;
if (fill !== details.lastFill) {
options.fill = fill;
details.lastFill = fill;
applyStyle = true;
}
if (outline !== details.lastOutline) {
options.weight = outline ? outlineWidth : 0.0;
details.lastOutline = outline;
applyStyle = true;
}
if (!Color.equals(fillColor, details.lastFillColor)) {
options.fillColor = fillColor.toCssColorString();
options.fillOpacity = fillColor.alpha;
Color.clone(fillColor, details.lastFillColor);
applyStyle = true;
}
if (!Color.equals(outlineColor, details.lastOutlineColor)) {
options.color = outlineColor.toCssColorString();
options.opacity = outlineColor.alpha;
Color.clone(outlineColor, details.lastOutlineColor);
applyStyle = true;
}
if (applyStyle) {
layer.setStyle(options);
}
}
private _updatePolyline(
entity: Entity,
time: JulianDate,
_entityHash: EntityHash,
entityDetails: EntityDetails
) {
const polylineGraphics = entity.polyline!;
const featureGroup = this._featureGroup;
let positions, polyline;
let details = entityDetails.polyline;
if (!isDefined(details)) {
details = entityDetails.polyline = {
layer: undefined
};
}
const geomLayer = details.layer;
let show =
entity.isAvailable(time) &&
getValueOrDefault(polylineGraphics.show, time, true);
if (show) {
positions = getValueOrUndefined(polylineGraphics.positions, time);
show = isDefined(positions);
}
if (!show) {
cleanPolyline(entity, featureGroup, entityDetails);
return;
}
const carts = Ellipsoid.WGS84.cartesianArrayToCartographicArray(positions);
const latlngs = [];
for (let p = 0; p < carts.length; p++) {
latlngs.push(
L.latLng(
CesiumMath.toDegrees(carts[p].latitude),
CesiumMath.toDegrees(carts[p].longitude)
)
);
}
let color;
let dashArray: number[] | undefined;
let width: number;
if (polylineGraphics.material instanceof PolylineGlowMaterialProperty) {
color = defaultColor;
width = defaultWidth;
} else {
const material = polylineGraphics.material.getValue(time);
if (isDefined(material)) {
color = material.color;
}
color = color || defaultColor;
width = getValueOrDefault(
polylineGraphics.width as unknown as Property,
time,
defaultWidth
);
}
if (polylineGraphics.material instanceof PolylineDashMaterialProperty) {
dashArray = getDashArray(polylineGraphics.material, time);
}
const polylineOptions: PolylineOptions = {
color: color.toCssColorString(),
weight: width,
opacity: color.alpha
};
if (dashArray) {
polylineOptions.dashArray = dashArray.map((x) => x * width).join(",");
}
if (!isDefined(geomLayer)) {
if (latlngs.length > 0) {
polyline = L.polyline(latlngs, polylineOptions);
polyline.on("click", featureClicked.bind(undefined, this, entity));
polyline.on(
"mousedown",
featureMousedown.bind(undefined, this, entity)
);
featureGroup.addLayer(polyline);
details.layer = polyline;
}
} else {
polyline = geomLayer;
const curLatLngs = polyline.getLatLngs();
let bPosChange = latlngs.length !== curLatLngs.length;
for (let i = 0; i < curLatLngs.length && !bPosChange; i++) {
const latlng = curLatLngs[i];
if (latlng instanceof L.LatLng && !latlng.equals(latlngs[i])) {
bPosChange = true;
}
}
if (bPosChange) {
polyline.setLatLngs(latlngs);
}
for (let prop in polylineOptions) {
if ((<any>polylineOptions)[prop] !== (<any>polyline.options)[prop]) {
polyline.setStyle(polylineOptions);
break;
}
}
}
}
/**
* Returns true if this object was destroyed; otherwise, false.
*
*/
isDestroyed(): boolean {
return false;
}
/**
* Removes and destroys all primitives created by this instance.
*/
destroy() {
const entities = this._entitiesToVisualize.values;
const entityHash = this._entityHash;
for (let i = entities.length - 1; i > -1; i--) {
cleanEntity(entities[i], this._featureGroup, entityHash);
}
this.entityCollection.collectionChanged.removeEventListener(
this._onCollectionChanged,
this
);
this.leafletScene.map.removeLayer(this._featureGroup);
return destroyObject(this);
}
/**
* Computes the rectangular bounds which encloses the collection of
* entities to be visualized.
*/
getLatLngBounds(): LatLngBounds | undefined {
let result: LatLngBounds | undefined;
Object.keys(this._entityHash).forEach((entityId) => {
const entityDetails: any = this._entityHash[entityId];
Object.keys(entityDetails).forEach((primitiveId) => {
const primitive = entityDetails[primitiveId];
if (isDefined(primitive.layer)) {
if (isDefined(primitive.layer.getBounds)) {
const bounds = primitive.layer.getBounds();
if (isDefined(bounds)) {
result =
result === undefined
? L.latLngBounds(bounds.getSouthWest(), bounds.getNorthEast())
: result.extend(bounds);
}
}
if (isDefined(primitive.layer.getLatLng)) {
const latLng = primitive.layer.getLatLng();
if (isDefined(latLng)) {
result =
result === undefined
? L.latLngBounds([latLng])
: result.extend(latLng);
}
}
}
});
});
return result;
}
}
function getDashArray(
material: PolylineDashMaterialProperty,
time: JulianDate
): number[] {
let dashArray;
const dashPattern = material.dashPattern
? material.dashPattern.getValue(time)
: undefined;
return getLineStyleLeaflet(dashPattern);
}
function cleanEntity(
entity: Entity,
group: L.FeatureGroup,
entityHash: EntityHash
) {
const details = entityHash[entity.id];
cleanPoint(entity, group, details);
cleanPolygon(entity, group, details);
cleanBillboard(entity, group, details);
cleanLabel(entity, group, details);
cleanPolyline(entity, group, details);
delete entityHash[entity.id];
}
function cleanPoint(
_entity: Entity,
group: L.FeatureGroup,
details: EntityDetails
) {
if (isDefined(details.point) && isDefined(details.point.layer)) {
group.removeLayer(details.point.layer);
details.point = undefined;
}
}
function cleanPolygon(
_entity: Entity,
group: L.FeatureGroup,
details: EntityDetails
) {
if (isDefined(details.polygon) && isDefined(details.polygon.layer)) {
group.removeLayer(details.polygon.layer);
details.polygon = undefined;
}
}
function cleanBillboard(
_entity: Entity,
group: L.FeatureGroup,
details: EntityDetails
) {
if (isDefined(details.billboard) && isDefined(details.billboard.layer)) {
group.removeLayer(details.billboard.layer);
details.billboard = undefined;
}
}
function cleanLabel(
_entity: Entity,
group: L.FeatureGroup,
details: EntityDetails
) {
if (isDefined(details.label) && isDefined(details.label.layer)) {
group.removeLayer(details.label.layer);
details.label = undefined;
}
}
function cleanPolyline(
_entity: Entity,
group: L.FeatureGroup,
details: EntityDetails
) {
if (isDefined(details.polyline) && isDefined(details.polyline.layer)) {
group.removeLayer(details.polyline.layer);
details.polyline = undefined;
}
}
function cleanRectangle(
_entity: Entity,
group: L.FeatureGroup,
details: EntityDetails
) {
if (isDefined(details.rectangle) && isDefined(details.rectangle.layer)) {
group.removeLayer(details.rectangle.layer);
details.rectangle = undefined;
}
}
function _isCloseToEasternAntiMeridian(bounds: LatLngBounds) {
const w = bounds.getWest();
const e = bounds.getEast();
if (w > 140 && (e < -140 || e > 180)) {
return true;
}
return false;
}
function _isCloseToWesternAntiMeridian(bounds: LatLngBounds) {
const w = bounds.getWest();
const e = bounds.getEast();
if ((w > 180 || w < -140) && e < -140) {
return true;
}
return false;
}
function positionToLatLng(
position: Cartesian3,
bounds: LatLngBounds | undefined
) {
var cartographic = Ellipsoid.WGS84.cartesianToCartographic(position);
let lon = CesiumMath.toDegrees(cartographic.longitude);
if (bounds !== undefined) {
if (_isCloseToEasternAntiMeridian(bounds)) {
if (lon < -140) {
lon = lon + 360;
}
} else if (_isCloseToWesternAntiMeridian(bounds)) {
if (lon > 140) {
lon = lon - 360;
}
}
}
return L.latLng(CesiumMath.toDegrees(cartographic.latitude), lon);
}
function hierarchyToLatLngs(hierarchy: PolygonHierarchy) {
let holes: L.LatLng[][] = [];
const positions = Array.isArray(hierarchy) ? hierarchy : hierarchy.positions;
if (hierarchy.holes.length > 0) {
hierarchy.holes.forEach((hole) => {
holes.push(convertEntityPositionsToLatLons(hole.positions));
});
return [convertEntityPositionsToLatLons(positions), ...holes];
} else {
return convertEntityPositionsToLatLons(positions);
}
}
//Recolor an image using 2d canvas
function recolorBillboard(
img: HTMLImageElement,
color: Color
): string | undefined {
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
// Copy the image contents to the canvas
const context = canvas.getContext("2d");
if (context === null) {
return;
}
context.drawImage(img, 0, 0);
const image = context.getImageData(0, 0, canvas.width, canvas.height);
const normClr = [color.red, color.green, color.blue, color.alpha];
const length = image.data.length; //pixel count * 4
for (let i = 0; i < length; i += 4) {
for (let j = 0; j < 4; j++) {
image.data[j + i] *= normClr[j];
}
}
context.putImageData(image, 0, 0);
return canvas.toDataURL();
}
function featureClicked(
visualizer: LeafletGeomVisualizer,
entity: Entity,
event: L.LeafletEvent
) {
visualizer.leafletScene.featureClicked.raiseEvent(entity, event);
}
function featureMousedown(
visualizer: LeafletGeomVisualizer,
entity: Entity,
event: L.LeafletEvent
) {
visualizer.leafletScene.featureMousedown.raiseEvent(entity, event);
}
function getValue<T>(
property: Property | undefined,
time: JulianDate
): T | undefined {
if (isDefined(property)) {
return property.getValue(time);
}
}
function getValueOrDefault<T>(
property: Property | undefined,
time: JulianDate,
defaultValue: T
): T {
if (isDefined(property)) {
const value = property.getValue(time);
if (isDefined(value)) {
return value;
}
}
return defaultValue;
}
function getValueOrUndefined(property: Property | undefined, time: JulianDate) {
if (isDefined(property)) {
return property.getValue(time);
}
}
function convertEntityPositionsToLatLons(positions: Cartesian3[]): L.LatLng[] {
var carts = Ellipsoid.WGS84.cartesianArrayToCartographicArray(positions);
var latlngs: L.LatLng[] = [];
let lastLongitude;
for (var p = 0; p < carts.length; p++) {
let lon = CesiumMath.toDegrees(carts[p].longitude);
if (lastLongitude !== undefined) {
if (lastLongitude - lon > 180) {
lon = lon + 360;
} else if (lastLongitude - lon < -180) {
lon = lon - 360;
}
}
latlngs.push(L.latLng(CesiumMath.toDegrees(carts[p].latitude), lon));
lastLongitude = lon;
}
return latlngs;
}
export default class LeafletVisualizer {
visualizersCallback(
leafletScene: LeafletScene,
_entityCluster: EntityCluster,
dataSource: DataSource
) {
const entities = dataSource.entities;
return [new LeafletGeomVisualizer(leafletScene, entities)];
}
}