Map/LeafletVisualizer.js

"use strict";

import i18next from "i18next";
/*global require*/
var L = require("leaflet");
var AssociativeArray = require("terriajs-cesium/Source/Core/AssociativeArray")
  .default;
var Cartesian2 = require("terriajs-cesium/Source/Core/Cartesian2").default;
var Cartesian3 = require("terriajs-cesium/Source/Core/Cartesian3").default;
var Cartographic = require("terriajs-cesium/Source/Core/Cartographic").default;
var CesiumMath = require("terriajs-cesium/Source/Core/Math").default;
var Color = require("terriajs-cesium/Source/Core/Color").default;
var defined = require("terriajs-cesium/Source/Core/defined").default;
var destroyObject = require("terriajs-cesium/Source/Core/destroyObject")
  .default;
var DeveloperError = require("terriajs-cesium/Source/Core/DeveloperError")
  .default;
var Ellipsoid = require("terriajs-cesium/Source/Core/Ellipsoid").default;
var Property = require("terriajs-cesium/Source/DataSources/Property").default;
var writeTextToCanvas = require("terriajs-cesium/Source/Core/writeTextToCanvas")
  .default;
var PolylineGlowMaterialProperty = require("terriajs-cesium/Source/DataSources/PolylineGlowMaterialProperty")
  .default;

var defaultColor = Color.WHITE;
var defaultOutlineColor = Color.BLACK;
var defaultOutlineWidth = 1.0;
var defaultPixelSize = 5.0;

var defaultWidth = 5.0;

//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 {@link Visualizer} which maps {@link Entity#point} to Leaflet primitives.
 * @alias LeafletGeomVisualizer
 * @constructor
 *
 * @param {LeafletScene} leafletScene The Leaflet scene that the the primitives will be rendered in.
 * @param {EntityCollection} entityCollection The entityCollection to visualize.
 */
var LeafletGeomVisualizer = function(leafletScene, entityCollection) {
  if (!defined(leafletScene)) {
    throw new DeveloperError(i18next.t("map.leafletVisualizer.devError1"));
  }
  if (!defined(entityCollection)) {
    throw new DeveloperError(i18next.t("map.leafletVisualizer.devError2"));
  }

  var featureGroup = L.featureGroup().addTo(leafletScene.map);
  entityCollection.collectionChanged.addEventListener(
    LeafletGeomVisualizer.prototype._onCollectionChanged,
    this
  );

  this._leafletScene = leafletScene;
  this._featureGroup = featureGroup;
  this._entityCollection = entityCollection;
  this._entitiesToVisualize = new AssociativeArray();
  this._entityHash = {};

  this._onCollectionChanged(entityCollection, entityCollection.values, [], []);
};

LeafletGeomVisualizer.prototype._onCollectionChanged = function(
  entityCollection,
  added,
  removed,
  changed
) {
  var i;
  var entity;
  var featureGroup = this._featureGroup;
  var entities = this._entitiesToVisualize;
  var entityHash = this._entityHash;

  for (i = added.length - 1; i > -1; i--) {
    entity = added[i];
    if (
      ((defined(entity._point) ||
        defined(entity._billboard) ||
        defined(entity._label)) &&
        defined(entity._position)) ||
      defined(entity._polyline) ||
      defined(entity._polygon)
    ) {
      entities.set(entity.id, entity);
      entityHash[entity.id] = {};
    }
  }

  for (i = changed.length - 1; i > -1; i--) {
    entity = changed[i];
    if (
      ((defined(entity._point) ||
        defined(entity._billboard) ||
        defined(entity._label)) &&
        defined(entity._position)) ||
      defined(entity._polyline) ||
      defined(entity._polygon)
    ) {
      entities.set(entity.id, entity);
      entityHash[entity.id] = entityHash[entity.id] || {};
    } else {
      cleanEntity(entity, featureGroup, entityHash);
      entities.remove(entity.id);
    }
  }

  for (i = removed.length - 1; i > -1; i--) {
    entity = removed[i];
    cleanEntity(entity, featureGroup, entityHash);
    entities.remove(entity.id);
  }
};

function cleanEntity(entity, group, entityHash) {
  var 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, group, details) {
  if (defined(details.point)) {
    group.removeLayer(details.point.layer);
    details.point = undefined;
  }
}

function cleanPolygon(entity, group, details) {
  if (defined(details.polygon)) {
    group.removeLayer(details.polygon.layer);
    details.polygon = undefined;
  }
}

function cleanBillboard(entity, group, details) {
  if (defined(details.billboard)) {
    group.removeLayer(details.billboard.layer);
    details.billboard = undefined;
  }
}

function cleanLabel(entity, group, details) {
  if (defined(details.label)) {
    group.removeLayer(details.label.layer);
    details.label = undefined;
  }
}

function cleanPolyline(entity, group, details) {
  if (defined(details.polyline)) {
    group.removeLayer(details.polyline.layer);
    details.polyline = undefined;
  }
}

/**
 * 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;

/**
 * Updates the primitives created by this visualizer to match their
 * Entity counterpart at the given time.
 *
 * @param {JulianDate} time The time to update to.
 * @returns {Boolean} This function always returns true.
 */
LeafletGeomVisualizer.prototype.update = function(time) {
  //>>includeStart('debug', pragmas.debug);
  if (!defined(time)) {
    throw new DeveloperError(i18next.t("map.leafletVisualizer.devError3"));
  }
  //>>includeEnd('debug');
  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;
  }

  var entities = this._entitiesToVisualize.values;
  var entityHash = this._entityHash;
  for (var i = 0, len = entities.length; i < len; i++) {
    var entity = entities[i];
    var entityDetails = entityHash[entity.id];

    if (defined(entity._point)) {
      this._updatePoint(
        entity,
        time,
        entityHash,
        entityDetails,
        applyLocalisedAntiMeridianFix === true ? bounds : null,
        prevBoundsType !== currentBoundsType
      );
    }
    if (defined(entity._billboard)) {
      this._updateBillboard(
        entity,
        time,
        entityHash,
        entityDetails,
        applyLocalisedAntiMeridianFix === true ? bounds : null
      );
    }
    if (defined(entity._label)) {
      this._updateLabel(entity, time, entityHash, entityDetails);
    }
    if (defined(entity._polyline)) {
      this._updatePolyline(entity, time, entityHash, entityDetails);
    }
    if (defined(entity._polygon)) {
      this._updatePolygon(entity, time, entityHash, entityDetails);
    }
  }
  prevBoundsType = currentBoundsType;
  return true;
};

/**
 * Computes the rectangular bounds which encloses the collection of
 * entities to be visualized.
 *
 * @returns {LatLngBounds} The computed bounds.
 */
LeafletGeomVisualizer.prototype.getLatLngBounds = function() {
  let result;

  Object.keys(this._entityHash).forEach(entityId => {
    const entityDetails = this._entityHash[entityId];

    Object.keys(entityDetails).forEach(primitiveId => {
      const primitive = entityDetails[primitiveId];

      if (defined(primitive.layer)) {
        if (defined(primitive.layer.getBounds)) {
          const bounds = primitive.layer.getBounds();
          if (defined(bounds)) {
            result =
              result === undefined
                ? L.latLngBounds(bounds.getSouthWest(), bounds.getNorthEast())
                : result.extend(bounds);
          }
        }
        if (defined(primitive.layer.getLatLng)) {
          const latLng = primitive.layer.getLatLng();
          if (defined(latLng)) {
            result =
              result === undefined
                ? L.latLngBounds([latLng])
                : result.extend(latLng);
          }
        }
      }
    });
  });

  return result;
};

/**
 * Computes whether we're looking close to the east of the anti-meridian
 * Our western viewing extent must be > 140 degrees
 * @returns boolean
 */
function isCloseToEasternAntiMeridian(bounds) {
  const w = bounds.getWest();
  const e = bounds.getEast();
  if (w > 140 && (e < -140 || e > 180)) {
    return true;
  }
  return false;
}

/**
 * Computes whether we're looking close to the west of the anti-meridian
 * Our eastern viewing extent must be < -140 degrees
 * @returns boolean
 */
function isCloseToWesternAntiMeridian(bounds) {
  const w = bounds.getWest();
  const e = bounds.getEast();
  if ((w > 180 || w < -140) && e < -140) {
    return true;
  }
  return false;
}

var cartographicScratch = new Cartographic();

function positionToLatLng(position, bounds) {
  var cartographic = Ellipsoid.WGS84.cartesianToCartographic(
    position,
    cartographicScratch
  );

  let lon = CesiumMath.toDegrees(cartographic.longitude);
  if (bounds !== null) {
    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);
}

LeafletGeomVisualizer.prototype._updatePoint = function(
  entity,
  time,
  entityHash,
  entityDetails,
  bounds,
  boundsJustChanged
) {
  var featureGroup = this._featureGroup;
  var pointGraphics = entity._point;

  var show =
    entity.isAvailable(time) &&
    Property.getValueOrDefault(pointGraphics._show, time, true);
  if (!show) {
    cleanPoint(entity, featureGroup, entityDetails);
    return;
  }

  var details = entityDetails.point;
  if (!defined(details)) {
    details = entityDetails.point = {
      layer: undefined,
      lastPosition: new Cartesian3(),
      lastPixelSize: 1,
      lastColor: new Color(),
      lastOutlineColor: new Color(),
      lastOutlineWidth: 1
    };
  }

  var position = Property.getValueOrUndefined(entity._position, time);
  if (!defined(position)) {
    cleanPoint(entity, featureGroup, entityDetails);
    return;
  }

  var pixelSize = Property.getValueOrDefault(
    pointGraphics._pixelSize,
    time,
    defaultPixelSize
  );
  var color = Property.getValueOrDefault(
    pointGraphics._color,
    time,
    defaultColor
  );
  var outlineColor = Property.getValueOrDefault(
    pointGraphics._outlineColor,
    time,
    defaultOutlineColor
  );
  var outlineWidth = Property.getValueOrDefault(
    pointGraphics._outlineWidth,
    time,
    defaultOutlineWidth
  );

  var layer = details.layer;

  if (!defined(layer)) {
    var 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;
  }

  var options = layer.options;
  var 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);
  }
  return layer;
};

LeafletGeomVisualizer.prototype._updatePolygon = function(
  entity,
  time,
  entityHash,
  entityDetails
) {
  var featureGroup = this._featureGroup;
  var polygonGraphics = entity._polygon;

  var show =
    entity.isAvailable(time) &&
    Property.getValueOrDefault(polygonGraphics._show, time, true);
  if (!show) {
    cleanPolygon(entity, featureGroup, entityDetails);
    return;
  }

  var details = entityDetails.polygon;
  if (!defined(details)) {
    details = entityDetails.polygon = {
      layer: undefined,
      lastHierarchy: undefined,
      lastFill: undefined,
      lastFillColor: new Color(),
      lastOutline: undefined,
      lastOutlineColor: new Color()
    };
  }

  var hierarchy = Property.getValueOrUndefined(
    polygonGraphics._hierarchy,
    time
  );
  if (!defined(hierarchy)) {
    cleanPolygon(entity, featureGroup, entityDetails);
    return;
  }

  var fill = Property.getValueOrDefault(polygonGraphics._fill, time, true);
  var outline = Property.getValueOrDefault(
    polygonGraphics._outline,
    time,
    true
  );
  var outlineColor = Property.getValueOrDefault(
    polygonGraphics._outlineColor,
    time,
    defaultOutlineColor
  );

  var material = Property.getValueOrUndefined(polygonGraphics._material, time);
  var fillColor;
  if (defined(material) && defined(material.color)) {
    fillColor = material.color;
  } else {
    fillColor = defaultColor;
  }

  var layer = details.layer;
  if (!defined(layer)) {
    var polygonOptions = {
      fill: fill,
      fillColor: fillColor.toCssColorString(),
      fillOpacity: fillColor.alpha,
      weight: outline ? 1.0 : 0.0,
      color: outlineColor.toCssColorString(),
      opacity: outlineColor.alpha
    };

    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.lastHierachy) {
    layer.setLatLngs(hierarchyToLatLngs(hierarchy));
    details.lastHierachy = hierarchy;
  }

  var options = layer.options;
  var applyStyle = false;

  if (fill !== details.lastFill) {
    options.fill = fill;
    details.lastFill = fill;
    applyStyle = true;
  }

  if (outline !== details.lastOutline) {
    options.weight = outline ? 1.0 : 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);
  }
};

function hierarchyToLatLngs(hierarchy) {
  // This function currently does not handle polygons with holes.

  var positions = Array.isArray(hierarchy) ? hierarchy : hierarchy.positions;

  return convertEntityPositionsToLatLons(positions);
}

//Recolor an image using 2d canvas
function recolorBillboard(img, color) {
  var canvas = document.createElement("canvas");
  canvas.width = img.width;
  canvas.height = img.height;

  // Copy the image contents to the canvas
  var context = canvas.getContext("2d");
  context.drawImage(img, 0, 0);
  var image = context.getImageData(0, 0, canvas.width, canvas.height);
  var normClr = [color.red, color.green, color.blue, color.alpha];

  var length = image.data.length; //pixel count * 4
  for (var i = 0; i < length; i += 4) {
    for (var j = 0; j < 4; j++) {
      image.data[j + i] *= normClr[j];
    }
  }

  context.putImageData(image, 0, 0);
  return canvas.toDataURL();
  //    return context.getImageData(0, 0, canvas.width, canvas.height);
}

//Single pixel black dot
var tmpImage =
  "data:image/gif;base64,R0lGODlhAQABAPAAAAAAAP///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";

//NYI: currently skipping all the camera distance related properties
LeafletGeomVisualizer.prototype._updateBillboard = function(
  entity,
  time,
  entityHash,
  entityDetails,
  bounds
) {
  var markerGraphics = entity._billboard;
  var featureGroup = this._featureGroup;
  var position, marker;

  var details = entityDetails.billboard;
  if (!defined(details)) {
    details = entityDetails.billboard = {
      layer: undefined
    };
  }

  var geomLayer = details.layer;

  var show =
    entity.isAvailable(time) &&
    Property.getValueOrDefault(markerGraphics._show, time, true);
  if (show) {
    position = Property.getValueOrUndefined(entity._position, time);
    show = defined(position);
  }
  if (!show) {
    cleanBillboard(entity, featureGroup, entityDetails);
    return;
  }

  var latlng = positionToLatLng(position, bounds);
  var image = Property.getValueOrDefault(
    markerGraphics._image,
    time,
    undefined
  );
  var height = Property.getValueOrDefault(
    markerGraphics._height,
    time,
    undefined
  );
  var width = Property.getValueOrDefault(
    markerGraphics._width,
    time,
    undefined
  );
  var color = Property.getValueOrDefault(
    markerGraphics._color,
    time,
    defaultColor
  );
  var scale = Property.getValueOrDefault(markerGraphics._scale, time, 1.0);
  var verticalOrigin = Property.getValueOrDefault(
    markerGraphics._verticalOrigin,
    time,
    0
  );
  var horizontalOrigin = Property.getValueOrDefault(
    markerGraphics._horizontalOrigin,
    time,
    0
  );
  var pixelOffset = Property.getValueOrDefault(
    markerGraphics._pixelOffset,
    time,
    Cartesian2.ZERO
  );

  var imageUrl;
  if (defined(image)) {
    if (typeof image === "string") {
      imageUrl = image;
    } else if (defined(image.toDataURL)) {
      imageUrl = image.toDataURL();
    } else if (defined(image.url)) {
      imageUrl = image.url;
    } else {
      imageUrl = image.src;
    }
  }

  var iconOptions = {
    color: color.toCssColorString(),
    origUrl: imageUrl,
    scale: scale,
    horizontalOrigin: horizontalOrigin, //value: left, center, right
    verticalOrigin: verticalOrigin //value: bottom, center, top
  };

  if (defined(height) || defined(width)) {
    iconOptions.iconSize = [width, height];
  }

  var redrawIcon = false;
  if (!defined(geomLayer)) {
    var 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._latlng.equals(latlng)) {
      marker.setLatLng(latlng);
    }
    for (var prop in iconOptions) {
      if (iconOptions[prop] !== marker.options.icon.options[prop]) {
        redrawIcon = true;
        break;
      }
    }
  }

  if (redrawIcon) {
    var drawBillboard = function(image, dataurl) {
      iconOptions.iconUrl = dataurl || image;
      if (!defined(iconOptions.iconSize)) {
        iconOptions.iconSize = [image.width * scale, image.height * scale];
      }
      var w = iconOptions.iconSize[0],
        h = iconOptions.iconSize[1];
      var xOff = (w / 2) * (1 - horizontalOrigin) - pixelOffset.x;
      var yOff = (h / 2) * (1 + verticalOrigin) - pixelOffset.y;
      iconOptions.iconAnchor = [xOff, yOff];

      if (!color.equals(defaultColor)) {
        iconOptions.iconUrl = recolorBillboard(image, color);
      }
      marker.setIcon(L.icon(iconOptions));
    };
    var img = new Image();
    img.onload = function() {
      drawBillboard(img, imageUrl);
    };
    img.src = imageUrl;
  }
};

LeafletGeomVisualizer.prototype._updateLabel = function(
  entity,
  time,
  entityHash,
  entityDetails
) {
  var labelGraphics = entity._label;
  var featureGroup = this._featureGroup;
  var position, marker;

  var details = entityDetails.label;
  if (!defined(details)) {
    details = entityDetails.label = {
      layer: undefined
    };
  }

  var geomLayer = details.layer;

  var show =
    entity.isAvailable(time) &&
    Property.getValueOrDefault(labelGraphics._show, time, true);
  if (show) {
    position = Property.getValueOrUndefined(entity._position, time);
    show = defined(position);
  }
  if (!show) {
    cleanLabel(entity, featureGroup, entityDetails);
    return;
  }

  var cart = Ellipsoid.WGS84.cartesianToCartographic(position);
  var latlng = L.latLng(
    CesiumMath.toDegrees(cart.latitude),
    CesiumMath.toDegrees(cart.longitude)
  );
  var text = Property.getValueOrDefault(labelGraphics._text, time, undefined);
  var font = Property.getValueOrDefault(labelGraphics._font, time, undefined);
  var scale = Property.getValueOrDefault(labelGraphics._scale, time, 1.0);
  var fillColor = Property.getValueOrDefault(
    labelGraphics._fillColor,
    time,
    defaultColor
  );
  var verticalOrigin = Property.getValueOrDefault(
    labelGraphics._verticalOrigin,
    time,
    0
  );
  var horizontalOrigin = Property.getValueOrDefault(
    labelGraphics._horizontalOrigin,
    time,
    0
  );
  var pixelOffset = Property.getValueOrDefault(
    labelGraphics._pixelOffset,
    time,
    Cartesian2.ZERO
  );

  var iconOptions = {
    text: text,
    font: font,
    color: fillColor.toCssColorString(),
    scale: scale,
    horizontalOrigin: horizontalOrigin, //value: left, center, right
    verticalOrigin: verticalOrigin //value: bottom, center, top
  };

  var redrawLabel = false;
  if (!defined(geomLayer)) {
    var 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._latlng.equals(latlng)) {
      marker.setLatLng(latlng);
    }
    for (var prop in iconOptions) {
      if (iconOptions[prop] !== marker.options.icon.options[prop]) {
        redrawLabel = true;
        break;
      }
    }
  }

  if (redrawLabel) {
    var drawBillboard = function(image, dataurl) {
      iconOptions.iconUrl = dataurl || image;
      if (!defined(iconOptions.iconSize)) {
        iconOptions.iconSize = [image.width * scale, image.height * scale];
      }
      var w = iconOptions.iconSize[0],
        h = iconOptions.iconSize[1];
      var xOff = (w / 2) * (1 - horizontalOrigin) - pixelOffset.x;
      var yOff = (h / 2) * (1 + verticalOrigin) - pixelOffset.y;
      iconOptions.iconAnchor = [xOff, yOff];
      marker.setIcon(L.icon(iconOptions));
    };

    var canvas = writeTextToCanvas(text, { fillColor: fillColor, font: font });
    var imageUrl = canvas.toDataURL();

    var img = new Image();
    img.onload = function() {
      drawBillboard(img, imageUrl);
    };
    img.src = imageUrl;
  }
};

function convertEntityPositionsToLatLons(positions) {
  var carts = Ellipsoid.WGS84.cartesianArrayToCartographicArray(positions);
  var latlngs = [];
  let lastLongitude = null;
  for (var p = 0; p < carts.length; p++) {
    let lon = CesiumMath.toDegrees(carts[p].longitude);

    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;
}

LeafletGeomVisualizer.prototype._updatePolyline = function(
  entity,
  time,
  entityHash,
  entityDetails
) {
  var polylineGraphics = entity._polyline;
  var featureGroup = this._featureGroup;
  var positions, polyline;

  var details = entityDetails.polyline;
  if (!defined(details)) {
    details = entityDetails.polyline = {
      layer: undefined
    };
  }

  var geomLayer = details.layer;

  var show =
    entity.isAvailable(time) &&
    Property.getValueOrDefault(polylineGraphics._show, time, true);
  if (show) {
    positions = Property.getValueOrUndefined(polylineGraphics._positions, time);
    show = defined(positions);
  }
  if (!show) {
    cleanPolyline(entity, featureGroup, entityDetails);
    return;
  }

  const latlngs = convertEntityPositionsToLatLons(positions);

  var color;
  var width;
  if (polylineGraphics._material instanceof PolylineGlowMaterialProperty) {
    color = defaultColor;
    width = defaultWidth;
  } else {
    color = Property.getValueOrDefault(
      polylineGraphics._material.color,
      time,
      defaultColor
    );
    width = Property.getValueOrDefault(
      polylineGraphics._width,
      time,
      defaultWidth
    );
  }

  var polylineOptions = {
    color: color.toCssColorString(),
    weight: width,
    opacity: color.alpha
  };

  if (!defined(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;
    var curLatLngs = polyline.getLatLngs();
    var bPosChange = latlngs.length !== curLatLngs.length;
    for (var i = 0; i < curLatLngs.length && !bPosChange; i++) {
      if (!curLatLngs[i].equals(latlngs[i])) {
        bPosChange = true;
      }
    }
    if (bPosChange) {
      polyline.setLatLngs(latlngs);
    }
    for (var prop in polylineOptions) {
      if (polylineOptions[prop] !== polyline.options[prop]) {
        polyline.setStyle(polylineOptions);
        break;
      }
    }
  }
};

/**
 * Returns true if this object was destroyed; otherwise, false.
 *
 * @returns {Boolean} True if this object was destroyed; otherwise, false.
 */
LeafletGeomVisualizer.prototype.isDestroyed = function() {
  return false;
};

/**
 * Removes and destroys all primitives created by this instance.
 */
LeafletGeomVisualizer.prototype.destroy = function() {
  var entities = this._entitiesToVisualize.values;
  var entityHash = this._entityHash;

  for (var i = entities.length - 1; i > -1; i--) {
    cleanEntity(entities[i], this._featureGroup, entityHash);
  }

  this._entityCollection.collectionChanged.removeEventListener(
    LeafletGeomVisualizer.prototype._onCollectionChanged,
    this
  );
  this._leafletScene.map.removeLayer(this._featureGroup);
  return destroyObject(this);
};

////////////////////////////////////////////////////////

var LeafletVisualizer = function() {};

LeafletVisualizer.prototype.visualizersCallback = function(
  leafletScene,
  entityCluster,
  dataSource
) {
  var entities = dataSource.entities;
  return [new LeafletGeomVisualizer(leafletScene, entities)];
};

function featureClicked(visualizer, entity, event) {
  visualizer._leafletScene.featureClicked.raiseEvent(entity, event);
}

function featureMousedown(visualizer, entity, event) {
  visualizer._leafletScene.featureMousedown.raiseEvent(entity, event);
}

module.exports = LeafletVisualizer;