UNPKG

terriajs

Version:

Geospatial data visualization platform.

1,353 lines (1,198 loc) 38.2 kB
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)]; } }