mirror of
https://github.com/openglobus/openglobus.git
synced 2025-12-08 19:25:27 +00:00
364 lines
12 KiB
JavaScript
364 lines
12 KiB
JavaScript
/**
|
|
* @module og/layer/KML
|
|
*/
|
|
|
|
"use strict";
|
|
import { Billboard } from "../entity/Billboard.js";
|
|
import { Entity } from "../entity/Entity.js";
|
|
import { Extent } from "../Extent.js";
|
|
import { LonLat } from "../LonLat.js";
|
|
import { Vector } from "./Vector.js";
|
|
|
|
/**
|
|
* Layer to render KMLs files
|
|
* @class
|
|
* @extends {Vector}
|
|
*/
|
|
export class KML extends Vector {
|
|
/**
|
|
*
|
|
* @param {string} name
|
|
* @param {*} [options]
|
|
*/
|
|
constructor(name, options = {}) {
|
|
super(name, options);
|
|
this._extent = null;
|
|
this._billboard = options.billboard || {
|
|
src: "https://openglobus.org/examples/billboards/carrot.png"
|
|
};
|
|
/**
|
|
* @type {string}
|
|
*/
|
|
this._color = options.color || "#6689db";
|
|
}
|
|
|
|
get instanceName() {
|
|
return "KML";
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
_extractCoordonatesFromKml(xmlDoc) {
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_AGBRtoRGBA(agbr) {
|
|
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})`;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
returns array of longitude, latitude, altitude (altitude optional)
|
|
*/
|
|
_parseKMLcoordinates(coords) {
|
|
const coordinates = coords.innerHTML.trim()
|
|
.replace(/\n/g, ' ')
|
|
.replace(/\t/g, ' ')
|
|
.replace(/ +/g, ' ')
|
|
.split(" ")
|
|
.map((co) => co.split(",").map(parseFloat))
|
|
|
|
return coordinates;
|
|
}
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_kmlPlacemarkToEntity(placemark, extent) {
|
|
if (!placemark) return;
|
|
|
|
const nameTags = Array.from(placemark.getElementsByTagName("name"))
|
|
const name = nameTags?.at(0)?.innerHTML?.trim() || '';
|
|
|
|
const { iconHeading, iconURL, iconColor, lineWidth, lineColor } = this._extractStyle(placemark);
|
|
|
|
// TODO handle MultiGeometry
|
|
|
|
const lonLats = [];
|
|
for (const coord of placemark.getElementsByTagName("coordinates")) {
|
|
const coordinates = this._parseKMLcoordinates(coord) || [[0, 0, 0]]
|
|
|
|
for (const lonlatalt of coordinates) {
|
|
const lon = lonlatalt[0];
|
|
const lat = lonlatalt[1];
|
|
const alt = lonlatalt[2];
|
|
|
|
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
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
_extractStyle(placemark) {
|
|
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 };
|
|
}
|
|
|
|
_parseKML(xml, extent, entities = undefined) {
|
|
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;
|
|
}
|
|
|
|
_convertKMLintoEntities(xml) {
|
|
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.
|
|
* @public
|
|
* @param {Array} coordonates
|
|
* @param {string} color
|
|
* @returns {Array<Entity>}
|
|
*/
|
|
_convertCoordonatesIntoEntities(coordinates, color, billboard) {
|
|
const extent = new Extent(new LonLat(180.0, 90.0), new LonLat(-180.0, -90.0));
|
|
const addToExtent = (c) => {
|
|
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 = [];
|
|
coordinates.forEach((kmlFile) => kmlFile.forEach((p) => _pathes.push(p)));
|
|
const entities = _pathes.map((path) => {
|
|
if (path.length === 1) {
|
|
const lonlat = path[0];
|
|
const _entity = new Entity({ lonlat, billboard });
|
|
addToExtent(lonlat);
|
|
return _entity;
|
|
} else if (path.length > 1) {
|
|
const pathLonLat = path.map((item) => {
|
|
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 };
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
* @returns {Document}
|
|
*/
|
|
_getXmlContent(file) {
|
|
return new Promise((resolve) => {
|
|
const fileReader = new FileReader();
|
|
fileReader.onload = async (i) =>
|
|
resolve(new DOMParser().parseFromString(i.target.result, "text/xml"));
|
|
fileReader.readAsText(file);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
_expandExtents(extent1, extent2) {
|
|
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}>}
|
|
*/
|
|
async addKmlFromFiles(kmls, color = null, billboard = null) {
|
|
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
|
|
*/
|
|
setColor(color) {
|
|
this._color = color;
|
|
this._billboard.color = color;
|
|
}
|
|
|
|
/**
|
|
* @public
|
|
*/
|
|
_getKmlFromUrl(url) {
|
|
return new Promise((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}>}
|
|
*/
|
|
async addKmlFromUrl(url, color = null, billboard = null) {
|
|
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 };
|
|
}
|
|
}
|