import {Billboard} from "../entity/Billboard";
import type {IBillboardParams} from "../entity/Billboard";
import {Entity} from "../entity/Entity";
import {Extent} from "../Extent";
import {LonLat} from "../LonLat";
import {Vector} from "./Vector";
import type {IVectorParams} from "./Vector";
import type {NumberArray3} from "../math/Vec3";
interface IKMLParams extends IVectorParams {
color?: string;
billboard?: IBillboardParams;
}
/**
* Layer to render KMLs files
* @class
* @extends {Vector}
* @param {string} name
* @param {*} [options]
*/
export class KML extends Vector {
protected _color: string;
protected _billboard: IBillboardParams;
constructor(name: string, options: IKMLParams = {}) {
super(name, options);
this._billboard = options.billboard || {
src: "https://openglobus.org/examples/billboards/carrot.png"
};
this._color = options.color || "#6689db";
}
public override get instanceName() {
return "KML";
}
protected _extractCoordonatesFromKml(xmlDoc: XMLDocument) {
const raw = Array.from(xmlDoc.getElementsByTagName("coordinates"));
const rawText = raw.map(item => item.textContent!.trim());
const coordinates = rawText.map(item =>
item
.replace(/\n/g, " ")
.replace(/\t/g, " ")
.replace(/ +/g, " ")
.split(" ")
.map((co) => co.split(",").map(parseFloat))
);
return coordinates;
}
protected _AGBRtoRGBA(agbr: string): string | undefined {
if (!agbr || agbr.length != 8) return;
const a = parseInt(agbr.slice(0, 2), 16) / 255;
const b = parseInt(agbr.slice(2, 4), 16);
const g = parseInt(agbr.slice(4, 6), 16);
const r = parseInt(agbr.slice(6, 8), 16);
return `rgba(${r},${g},${b},${a})`;
}
/**
* @protected
* @returns array of longitude, latitude, altitude (altitude optional)
*/
protected _parseKMLcoordinates(coords: Element): number[][] {
const coordinates = coords.innerHTML.trim()
.replace(/\n/g, ' ')
.replace(/\t/g, ' ')
.replace(/ +/g, ' ')
.split(" ")
.map((co) => co.split(",").map(parseFloat))
return coordinates;
}
protected _kmlPlacemarkToEntity(placemark: Element | undefined | null, extent: Extent): Entity | undefined {
if (!placemark) return;
const nameTags: Element[] = Array.from(placemark.getElementsByTagName("name"))
const name: string = nameTags && nameTags.length > 0 ? nameTags[0].innerHTML.trim() : '';
const {iconHeading, iconURL, iconColor, lineWidth, lineColor} = this._extractStyle(placemark);
// TODO handle MultiGeometry
const lonLats: LonLat[] = [];
for (const coord of placemark.getElementsByTagName("coordinates")) {
const coordinates = this._parseKMLcoordinates(coord) || [[0, 0, 0]]
for (const lonlatalt of coordinates) {
const [lon, lat, alt] = lonlatalt
lonLats.push(new LonLat(lon, lat, alt));
if (lon < extent.southWest.lon) extent.southWest.lon = lon;
if (lat < extent.southWest.lat) extent.southWest.lat = lat;
if (lon > extent.northEast.lon) extent.northEast.lon = lon;
if (lat > extent.northEast.lat) extent.northEast.lat = lat;
}
}
if (lonLats.length === 1) {
const hdgrad = iconHeading * 0.01745329; // radians
return new Entity({
name,
lonlat: lonLats[0],
billboard: {
src: iconURL,
size: [24, 24],
color: iconColor,
rotation: hdgrad
},
properties: {
color: iconColor,
heading: iconHeading
}
});
} else {
return new Entity({
polyline: {
pathLonLat: [lonLats],
thickness: lineWidth,
color: lineColor,
isClosed: false
}
});
}
}
protected _extractStyle(placemark: Element): any {
let iconColor;
let iconHeading;
let iconURL;
let lineColor;
let lineWidth;
const style = placemark.getElementsByTagName("Style")[0];
if (style) {
let iconstyle = style.getElementsByTagName("IconStyle")[0];
if (iconstyle) {
let color = iconstyle.getElementsByTagName("color")[0];
if (color)
iconColor = this._AGBRtoRGBA(color.innerHTML.trim());
let heading = iconstyle.getElementsByTagName("heading")[0];
if (heading) {
const hdg = parseFloat(heading.innerHTML.trim());
if (hdg >= 0 && hdg <= 360)
iconHeading = hdg % 360;
}
let icon = iconstyle.getElementsByTagName("Icon")[0];
if (icon) {
let href = icon.getElementsByTagName("href")[0];
if (href) {
iconURL = href.innerHTML.trim();
}
}
}
let linestyle = style.getElementsByTagName("LineStyle")[0];
if (linestyle) {
let color = linestyle.getElementsByTagName("color")[0];
if (color)
lineColor = this._AGBRtoRGBA(color.innerHTML.trim());
let width = linestyle.getElementsByTagName("width")[0];
if (width !== undefined)
lineWidth = parseFloat(width.innerHTML.trim());
}
}
if (!iconColor) iconColor = "#FFFFFF"
if (!iconHeading) iconHeading = 0
if (!iconURL) iconURL = "https://openglobus.org/examples/billboards/carrot.png"
if (!lineColor) lineColor = "#FFFFFF"
if (!lineWidth) lineWidth = 1
return {iconHeading, iconURL, iconColor, lineWidth, lineColor};
}
protected _parseKML(xml: XMLDocument, extent: Extent, entities?: Entity[]): Entity[] {
if (!entities)
entities = [];
if (xml.documentElement.nodeName !== "kml")
return entities;
for (const placemark of xml.getElementsByTagName("Placemark")) {
const entity = this._kmlPlacemarkToEntity(placemark, extent);
if (entity) entities.push(entity);
}
return entities;
}
protected _convertKMLintoEntities(xml: XMLDocument): any {
const extent = new Extent(new LonLat(180.0, 90.0), new LonLat(-180.0, -90.0));
const entities = this._parseKML(xml, extent);
return {entities, extent}
}
/**
* Creates billboards or polylines from array of lonlat.
* @protected
* @param {Array} coordonates
* @param {string} color
* @returns {any}
*/
protected _convertCoordonatesIntoEntities(coordinates: number[][][][], color: string, billboard?: IBillboardParams): any {
const extent = new Extent(new LonLat(180.0, 90.0), new LonLat(-180.0, -90.0));
const addToExtent = (c: number[]) => {
const lon = c[0],
lat = c[1];
if (lon < extent.southWest.lon) extent.southWest.lon = lon;
if (lat < extent.southWest.lat) extent.southWest.lat = lat;
if (lon > extent.northEast.lon) extent.northEast.lon = lon;
if (lat > extent.northEast.lat) extent.northEast.lat = lat;
};
const _pathes: number[][][] = [];
coordinates.forEach((kmlFile: number[][][]) => kmlFile.forEach((p: number[][]) => _pathes.push(p)));
const entities = _pathes.map((path) => {
if (path.length === 1) {
const lonlat = path[0] as NumberArray3;
const _entity = new Entity({lonlat, billboard});
addToExtent(lonlat);
return _entity;
} else if (path.length > 1) {
const pathLonLat = path.map((item: number[]) => {
addToExtent(item);
return new LonLat(item[0], item[1], item[2]);
});
const _entity = new Entity({
polyline: {pathLonLat: [pathLonLat], thickness: 3, color, isClosed: false}
});
return _entity;
}
});
return {entities, extent};
}
/**
* @protected
* @returns {Document}
*/
protected _getXmlContent(file: Blob): Promise<XMLDocument> {
return new Promise((resolve) => {
const fileReader = new FileReader();
fileReader.onload = async (i) =>
resolve(new DOMParser().parseFromString(i.target!.result as string, "text/xml"));
fileReader.readAsText(file);
});
}
protected _expandExtents(extent1: Extent | null | undefined, extent2: Extent): Extent {
if (!extent1) return extent2;
if (extent2.southWest.lon < extent1.southWest.lon)
extent1.southWest.lon = extent2.southWest.lon;
if (extent2.southWest.lat < extent1.southWest.lat)
extent1.southWest.lat = extent2.southWest.lat;
if (extent2.northEast.lon > extent1.northEast.lon)
extent1.northEast.lon = extent2.northEast.lon;
if (extent2.northEast.lat > extent1.northEast.lat)
extent1.northEast.lat = extent2.northEast.lat;
return extent1;
}
/**
* @public
* @param {File[]} kmls
* @param {string} [color]
* @param {Billboard} [billboard]
* @returns {Promise<{entities: Entity[], extent: Extent}>}
*/
public async addKmlFromFiles(kmls: Blob[], color?: string, billboard?: IBillboardParams) {
if (!Array.isArray(kmls)) return null
const kmlObjs = await Promise.all(kmls.map(this._getXmlContent));
const coordonates = kmlObjs.map(this._extractCoordonatesFromKml);
const {entities, extent} = this._convertCoordonatesIntoEntities(
coordonates,
color || this._color,
billboard || this._billboard
);
this._extent = this._expandExtents(this._extent, extent);
entities.forEach(this.add.bind(this));
return {entities, extent};
}
/**
* @param {string} color
* @public
*/
public setColor(color: string) {
this._color = color;
this._billboard.color = color;
}
protected _getKmlFromUrl(url: string): Promise<Document> {
return new Promise<Document>((resolve, reject) => {
const request = new XMLHttpRequest();
request.open("GET", url, true);
request.responseType = "document";
request.overrideMimeType("text/xml");
request.onload = () => {
if (request.readyState === request.DONE && request.status === 200) {
resolve(request.responseXML!);
} else {
reject(new Error("no valid kml file"));
}
};
request.send();
});
}
/**
* @public
* @param {string} url - Url of the KML to display. './myFile.kml' or 'http://mySite/myFile.kml' for example.
* @param {string} [color]
* @param {Billboard} [billboard]
* @returns {Promise<{entities: Entity[], extent: Extent}>}
*/
public async addKmlFromUrl(url: string, color?: string, billboard?: Billboard | IBillboardParams): Promise<any> {
const kml = await this._getKmlFromUrl(url);
/*
const coordonates = this._extractCoordonatesFromKml(kml);
const { entities, extent } = this._convertCoordonatesIntoEntities(
[coordonates],
color || this._color,
billboard || this._billboard
);
*/
const {entities, extent} = this._convertKMLintoEntities(kml);
this._extent = this._expandExtents(this._extent, extent);
entities.forEach(this.add.bind(this));
return {entities, extent};
}
}