UNPKG

leaflet-kmz

Version:

A KMZ file loader for Leaflet Maps

810 lines (700 loc) 25.2 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('leaflet-pointable')) : typeof define === 'function' && define.amd ? define(['exports', 'leaflet-pointable'], factory) : (global = global || self, factory(global['leaflet-kmz'] = {})); }(this, function (exports) { 'use strict'; L.KMZParser = L.Class.extend({ initialize: function(opts) { L.setOptions(this, opts); this.loaders = []; }, load: function(kmzUrl, opts) { this._loadAsyncJS(this._requiredJSModules()); // async download all required JS modules. this._waitAsyncJS(this._loadKMZ.bind(this, kmzUrl, opts)); // wait until all JS modules are downloaded. }, get: function(i) { return i < this.loaders.length ? this.loaders[i] : false; }, _loadKMZ: function(kmzUrl, opts) { var kmzLoader = new L.KMZLoader(L.extend({}, this.options, opts)); kmzLoader.parse(kmzUrl); this.loaders.push(kmzLoader); }, _loadAsyncJS: function(urls) { if (!L.KMZParser._jsPromise && urls.length) { var promises = urls.map(url => this._loadJS(url)); L.KMZParser._jsPromisePending = true; L.KMZParser._jsPromise = Promise.all(promises).then(function() { L.KMZParser._jsPromisePending = false; }.bind(this)); } }, _loadJS: function(url) { return new Promise(function(resolve, reject) { var tag = document.createElement("script"); tag.type = "text/javascript"; tag.src = url; tag.onload = resolve.bind(url); tag.onerror = reject.bind(url); document.head.appendChild(tag); }); }, _requiredJSModules: function() { var urls = []; var host = 'https://unpkg.com/'; if (typeof JSZip !== 'function' && typeof window.JSZip !== 'function') { urls.push(host + 'jszip@3.1.5/dist/jszip.min.js'); } if (typeof toGeoJSON !== 'object' && typeof window.toGeoJSON !== 'object') { urls.push(host + '@tmcw/togeojson@3.0.1/dist/togeojsons.min.js'); } if (typeof geojsonvt !== 'function' && typeof window.geojsonvt !== 'function') { urls.push(host + 'geojson-vt@3.0.0/geojson-vt.js'); } return urls; }, _waitAsyncJS: function(callback) { if (L.KMZParser._jsPromise && L.KMZParser._jsPromisePending) { L.KMZParser._jsPromise.then(callback); } else { callback.call(); } }, }); var KMZParser = L.KMZParser; // import JSZip from 'jszip'; // import geojsonvt from 'geojson-vt'; // import * as toGeoJSON from '@tmcw/togeojson'; L.KMZLoader = L.Class.extend({ options: { renderer: true, tiled: true, interactive: true, ballon: true, bindPopup: true, bindTooltip: true, debug: 0, keepFront: true, emptyIcon: "data:image/png;" + "base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAFElEQVR4XgXAAQ0AAABAMP1L30IDCPwC/o5WcS4AAAAASUVORK5CYII=" }, initialize: function(opts) { L.setOptions(this, opts); this.renderer = this.options.renderer; this.tiled = this.options.tiled; // (Optimized) GeoJSON Vector Tiles ["geojson-vt.js"] library. this.interactive = this.options.interactive; // (Default) Mouse interactions through ["leaflet.js"] layers. this.pointable = this.tiled && !this.interactive && this.options.pointable; // (Experimental) Optimized Mouse interactions through ["geojson-vt.js", "leaflet-pointable.js"] libraries. this.emptyIcon = this.options.emptyIcon; this.name = this.options.name; this.callback = opts.onKMZLoaded; }, parse: function(kmzUrl) { this.name = this.name ? this.name : kmzUrl.split('/').pop(); this._load(kmzUrl); }, _load: function(url) { this._getBinaryContent(url, function(err, data) { if (err != null) console.error(url, err, data); else this._parse(data); }.bind(this)); }, _parse: function(data) { return this._isZipped(data) ? this._parseKMZ(data) : this._parseKML(data); }, _parseKMZ: function(data) { var that = this; JSZip.loadAsync(data).then((zip) => { Promise.all(that._mapZipFiles(zip)).then((list) => { Promise.all(that._mapListFiles(list)).then((data) => { var kmlString = this._decodeKMZFolder(data); that._parseKML(kmlString); }); }); }); }, _parseKML: function(data) { var kmlString = this._decodeKMLString(data); var xmlDoc = this._toXML(kmlString); this._kmlToLayer(xmlDoc); }, _decodeKMLString: function(data) { return data instanceof ArrayBuffer ? String.fromCharCode.apply(null, new Uint8Array(data)) : data; }, _decodeKMZFolder: function(data) { var kmzFiles = this._listToObject(data); var kmlDoc = this._getKmlDoc(kmzFiles); var images = this._getImageFiles(Object.keys(kmzFiles)); var kmlString = kmzFiles[kmlDoc]; // replaces all images with their base64 encoding for (var i in images) { var imageUrl = images[i]; var dataUrl = kmzFiles[imageUrl]; kmlString = this._replaceAll(kmlString, imageUrl, dataUrl); } return kmlString; }, _toXML: function(text) { return (new DOMParser()).parseFromString(text, 'text/xml'); }, _toGeoJSON: function(xmlDoc) { return (toGeoJSON || window.toGeoJSON).kml(xmlDoc); }, _keepFront: function(layer) { var keepFront = function(e) { if (this.bringToFront) this.bringToFront(); }.bind(layer); layer.on('add', function(e) { this._map.on('baselayerchange', keepFront); }); layer.on('remove', function(e) { this._map.off('baselayerchange', keepFront); }); }, _kmlToLayer: function(xmlDoc) { var data = this._toGeoJSON(xmlDoc); if (this.interactive) { this.geojson = L.geoJson(data, { pointToLayer: this._pointToLayer.bind(this), onEachFeature: this._onEachFeature.bind(this), kmzRenderer: this.renderer, }); this.layer = this.geojson; } if (this.tiled) { this.gridlayer = L.gridLayer.geoJson(data, { pointable: this.pointable, ballon: this.options.ballon, bindPopup: this.options.bindPopup, bindTooltip: this.options.bindTooltip, }); this.layer = this.interactive ? L.featureGroup([this.gridlayer, this.geojson]) : this.gridlayer; } if (this.layer) { this._onKMZLoaded(this.layer, this.name); } }, _pointToLayer: function(feature, latlng) { return new L.KMZMarker(latlng, { kmzRenderer: this.renderer, }); // return new L.marker(latlng, { // icon: L.icon({ // iconUrl: this.emptyIcon, // }), // }); }, _onEachFeature: function(feature, layer) { switch (feature.geometry.type) { case 'Point': this._setLayerPointIcon(feature, layer); break; case 'LineString': case 'Polygon': case 'GeometryCollection': this._setLayerStyle(feature, layer); break; default: console.warn('Unsupported feature type: ' + feature.geometry.type, feature); break; } this._setLayerBalloon(feature, layer); }, _onKMZLoaded: function(layer, name) { if (this.options.debug) console.log(layer, name); if (this.options.keepFront) this._keepFront(layer); if (this.callback) this.callback(layer, name); }, _setLayerPointIcon: function(feature, layer) { layer.setIconUrl(this.tiled ? this.emptyIcon : feature.properties.icon); // var width = 28; // var height = 28; // layer.setIcon(L.icon({ // iconSize: [width, height], // iconAnchor: [width / 2, height / 2], // iconUrl: this.tiled ? this.emptyIcon : feature.properties.icon, // })); }, _setLayerStyle: function(feature, layer) { var styles = { weight: 1, opacity: 0, fillOpacity: 0, }; if (!this.tiled) { if (feature.properties["stroke-width"]) { styles.weight = feature.properties["stroke-width"] * 1.05; } if (feature.properties["stroke-opacity"]) { styles.opacity = feature.properties["stroke-opacity"]; } if (feature.properties["fill-opacity"]) { styles.fillOpacity = feature.properties["fill-opacity"]; } if (feature.properties.stroke) { styles.color = feature.properties.stroke; } if (feature.properties.fill) { styles.fillColor = feature.properties.fill; } } layer.setStyle(styles); }, _setLayerBalloon: function(feature, layer) { if (!this.options.ballon) return; var name = feature.properties.name ? feature.properties.name : ""; var desc = feature.properties.description ? feature.properties.description : ""; if (name || desc) { if (this.options.bindPopup) { layer.bindPopup('<div>' + '<b>' + name + '</b>' + '<br>' + desc + '</div>'); } if (this.options.bindTooltip) { layer.bindTooltip('<b>' + name + '</b>', { direction: 'auto', sticky: true, }); } } }, _escapeRegExp: function(str) { return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); }, _replaceAll: function(str, find, replace) { return str.replace(new RegExp(this._escapeRegExp(find), 'g'), replace); }, _mapZipFiles: function(zip) { return Object.keys(zip.files) .map((name) => zip.files[name]) .map((entry) => entry .async("blob") .then((value) => [entry.name, value]) // [ fileName, stringValue ] ); }, _mapListFiles: function(list) { return list.map(file => Promise.resolve().then(() => { return this._readFile(file); })); }, _listToObject: function(list) { return list .reduce(function(newObj, listElem) { newObj[listElem[0]] = listElem[1]; // { fileName: stringValue } return newObj; }, {} /* NB: do not remove, initial value */ ); }, _getFileExt: function(filename) { return filename.split('.').pop().toLowerCase().replace('jpg', 'jpeg'); }, _getMimeType: function(filename, ext) { var mime = 'text/plain'; if (/\.(jpe?g|png|gif|bmp)$/i.test(filename)) { mime = 'image/' + ext; } else if (/\.kml$/i.test(filename)) { mime = 'text/plain'; } return mime; }, _getKmlDoc: function(files) { return files["doc.kml"] ? "doc.kml" : this._getKmlFiles(Object.keys(files))[0]; }, _getKmlFiles: function(files) { return files.filter((file) => /.*\.kml/.test(file)); }, _getImageFiles: function(files) { return files.filter((file) => /\.(jpe?g|png|gif|bmp)$/i.test(file)); }, /** * It checks if a given file begins with PK, if so it's zipped * * @link https://en.wikipedia.org/wiki/List_of_file_signatures */ _isZipped: function(file) { var P = new Uint8Array(file, 0, 1); // offset, length var K = new Uint8Array(file, 1, 1); var PK = String.fromCharCode(P, K); return 'PK' === PK; }, _readFile: function(file) { var filename = file[0]; var fileblob = file[1]; var ext = this._getFileExt(filename); var mime = this._getMimeType(filename, ext); return this._fileReader(fileblob, mime, filename); }, _fileReader: function(blob, mime, name) { return new Promise((resolve, reject) => { var fr = new FileReader(); fr.onload = () => { var result = fr.result; if (mime.indexOf('text') === -1) { var dataUrl = fr.result; var base64 = dataUrl.split(',')[1]; result = 'data:' + mime + ';base64,' + base64; } return resolve([ name, result ]); }; if (mime.indexOf('text') === -1) { fr.readAsDataURL(blob); } else { fr.readAsText(blob); } }); }, _getBinaryContent: function(path, callback) { try { var xhr = new window.XMLHttpRequest(); xhr.open('GET', path, true); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.responseType = "arraybuffer"; xhr.onreadystatechange = function(evt) { var file, err; if (xhr.readyState === 4) { if (xhr.status === 200 || xhr.status === 0) { file = null; err = null; try { file = xhr.response || xhr.responseText; } catch (e) { err = new Error(e); } callback(err, file); } else { callback(new Error("Ajax error for " + path + " : " + this.status + " " + this.statusText), null); } } }; xhr.send(); } catch (e) { callback(new Error(e), null); } }, _blobToString: function(b) { var u, x; u = URL.createObjectURL(b); x = new XMLHttpRequest(); x.open('GET', u, false); // although sync, you're not fetching over internet x.send(); URL.revokeObjectURL(u); return x.responseText; }, _blobToBase64: function(blob, callback) { var reader = new FileReader(); reader.onload = function() { var dataUrl = reader.result; var base64 = dataUrl.split(',')[1]; callback(base64); }; reader.readAsDataURL(blob); }, }); /** * Include a default canvas renderer to each initialized map. */ var mapProto = L.Map.prototype; var getRendererMapProto = mapProto.getRenderer; L.Map.addInitHook(function() { this.options.kmzRenderer = L.canvas({ padding: 0.5 /*, pane: 'overlayPane'*/ }); }); L.Map.include({ getRenderer: function(layer) { if (layer && layer.options && layer.options.kmzRenderer) { if (layer.options.kmzRenderer instanceof L.Renderer) layer.options.renderer = layer.options.kmzRenderer; else if (layer.options.kmzRenderer) layer.options.renderer = this.options.kmzRenderer; } var renderer = getRendererMapProto.call(this, layer); return renderer; }, }); var KMZLoader = L.KMZLoader; /** * Optimized leaflet canvas renderer to load numerous markers * * @link https://stackoverflow.com/a/51852641 * @link https://stackoverflow.com/a/43019740 * */ L.KMZMarker = L.CircleMarker.extend({ setIconUrl: function(iconUrl) { this._iconUrl = typeof iconUrl !== "undefined" ? iconUrl : this._iconUrl; }, _updatePath: function() { var renderer = this._renderer; var layer = this; if (!this._iconUrl || !renderer._drawing || layer._empty()) { return; } var p = layer._point, ctx = renderer._ctx; var icon = new Image(), width = 28, height = 28; icon.onload = function() { ctx.drawImage(icon, p.x - (width / 2.0), p.y - (height / 2.0), width, height); renderer._drawnLayers[layer._leaflet_id] = layer; }; icon.src = this._iconUrl; } }); var KMZMarker = L.KMZMarker; /** * A plugin combining geojson-vt with leafletjs which is initially inspired by leaflet-geojson-vt. * * @author Brandonxiang, Raruto * * @link https://github.com/brandonxiang/leaflet-geojson-vt */ L.GridLayer.GeoJSON = L.GridLayer.extend({ options: { pointable: false, ballon: false, bindPopup: false, bindTooltip: false, async: false, maxZoom: 24, tolerance: 3, debug: 0, extent: 4096, buffer: 256, icon: { width: 28, height: 28 }, styles: { strokeWidth: 1, strokeColor: '#f00', strokeOpacity: 1.0, fillColor: '#000', fillOpacity: 0.25 } }, initialize: function(geojson, options) { L.setOptions(this, options); L.GridLayer.prototype.initialize.call(this, options); this.tileIndex = (geojsonvt || window.geojsonvt)(geojson, this.options); this.geojson = geojson; // eg. saved for advanced "leaflet-pip" mouse/click integrations }, onAdd: function(map) { L.GridLayer.prototype.onAdd.call(this, map); if (this.options.ballon) { if (this.options.bindPopup) this._map.on("click", this.updateBalloon, this); if (this.options.bindTooltip) this._map.on("mousemove", this.updateBalloon, this); } }, createTile: function(coords) { var tile = L.DomUtil.create('canvas', 'leaflet-tile'); var size = this.getTileSize(); tile.width = size.x; tile.height = size.y; var ctx = tile.getContext('2d'); // return the tile so it can be rendered on screen var tileInfo = this.tileIndex.getTile(coords.z, coords.x, coords.y); var features = tileInfo ? tileInfo.features : []; for (var i = 0; i < features.length; i++) { this._drawFeature(ctx, features[i]); } return tile; }, _drawFeature: function(ctx, feature) { ctx.beginPath(); this._setStyle(ctx, feature); if (feature.type === 1) this._drawIcon(ctx, feature); else if (feature.type === 2) this._drawLine(ctx, feature); else if (feature.type === 3) this._drawPolygon(ctx, feature); else console.warn('Unsupported feature type: ' + feature.geometry.type, feature); ctx.stroke(); }, _drawIcon: function(ctx, feature) { var icon = new Image(), p = feature.geometry[0], width = this.options.icon.width, height = this.options.icon.height; icon.onload = function() { ctx.drawImage(icon, (p[0] / 16.0) - (width / 2.0), (p[1] / 16.0) - (height / 2.0), width, height); }; icon.src = feature.tags.icon ? feature.tags.icon : null; }, _drawLine: function(ctx, feature) { for (var j = 0; j < feature.geometry.length; j++) { var ring = feature.geometry[j]; for (var k = 0; k < ring.length; k++) { var p = ring[k]; if (k) ctx.lineTo(p[0] / 16.0, p[1] / 16.0); else ctx.moveTo(p[0] / 16.0, p[1] / 16.0); } } }, _drawPolygon: function(ctx, feature) { this._drawLine(ctx, feature); ctx.fill('evenodd'); }, _setStyle: function(ctx, feature) { var style = {}; if (feature.type === 1) style = this._setPointStyle(feature, style); else if (feature.type === 2) style = this._setLineStyle(feature, style); else if (feature.type === 3) style = this._setPolygonStyle(feature, style); ctx.lineWidth = style.stroke ? this._setWeight(style.weight) : 0; ctx.strokeStyle = style.stroke ? this._setOpacity(style.stroke, style.opacity) : {}; ctx.fillStyle = style.fill ? this._setOpacity(style.fill, style.fillOpacity) : {}; }, _setPointStyle: function(feature, style) { return style; }, _setLineStyle: function(feature, style) { style.weight = (feature.tags["stroke-width"] ? feature.tags["stroke-width"] : this.options.styles.strokeWidth) * 1.05; style.opacity = feature.tags["stroke-opacity"] ? feature.tags["stroke-opacity"] : this.options.styles.strokeOpacity; style.stroke = feature.tags.stroke ? feature.tags.stroke : this.options.styles.strokeColor; return style; }, _setPolygonStyle: function(feature, style) { style = this._setLineStyle(feature, style); style.fill = feature.tags.fill ? feature.tags.fill : this.options.styles.fillColor; style.fillOpacity = feature.tags["fill-opacity"] ? feature.tags["fill-opacity"] : this.options.styles.fillOpacity; return style; }, _setWeight: function(weight) { return weight || 5; }, _setOpacity: function(color, opacity) { color = color || '#f00'; if (opacity && this._iscolorHex(color)) { var colorRgb = this._colorRgb(color); return "rgba(" + colorRgb[0] + "," + colorRgb[1] + "," + colorRgb[2] + "," + opacity + ")"; } return color; }, _iscolorHex: function(color) { return /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(color.toLowerCase()); }, _colorRgb: function(color) { var sColor = color.toLowerCase(); if (sColor.length === 4) { var sColorNew = "#"; for (var i = 1; i < 4; i += 1) { sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1)); } sColor = sColorNew; } var sColorChange = []; for (var j = 1; j < 7; j += 2) { sColorChange.push(parseInt("0x" + sColor.slice(j, j + 2))); } return sColorChange; }, /** * Point in Polygon: ray-casting algorithm * * @link http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html */ _pointInPolygon: function(point, vs) { var x = point[0]; var y = point[1]; var inside = false; for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) { var xi = vs[i][0]; var yi = vs[i][1]; var xj = vs[j][0]; var yj = vs[j][1]; var intersect = ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi); if (intersect) inside = !inside; } return inside; }, _getLatLngsPoly: function(feature, i) { var o = []; var geometry = feature.geometry || feature; var coords = geometry.type == "Polygon" ? geometry.coordinates[0] : geometry.coordinates; for (var j = i || 0; j < coords.length; j++) { o[i++] = [coords[j][0], coords[j][1]]; } return o.length ? o : false; }, _getLatLngsPoint: function(feature, i) { var o = []; var geometry = feature.geometry || feature; var coords = geometry.coordinates; o[i || 0] = [coords[0], coords[1]]; return o.length ? o : false; }, _getLatLngs: function(feature, i) { var o = []; i = i || 0; var coords; var geometry = feature.geometry || feature; var type = geometry.type; if (type == "Point") { coords = this._getLatLngsPoint(feature, i); if (coords) Array.prototype.push.apply(o, coords); } else if (type == "LineString" || type == "Polygon") { coords = this._getLatLngsPoly(feature, i); if (coords) Array.prototype.push.apply(o, coords); } else if (type == "GeometryCollection") { var polys = geometry.geometries; for (var j = 0; j < polys.length; j++) { coords = this._getLatLngs(polys[j], i); if (coords) Array.prototype.push.apply(o, coords); } } else { console.warn("Unsupported feature type: " + type); } return o.length ? o : false; }, /** * (EXPERIMENTAL) Inspired by: https://github.com/mapbox/leaflet-pip * * TODO: add/check support for Points, Lines and "donuts" Polygons */ pointInLayer: function(p, layer, first) { if (p instanceof L.LatLng) p = [p.lng, p.lat]; var results = []; layer = layer || this.geojson; first = first || true; var features = layer.features; for (var i = 0; i < features.length; i++) { if (first && results.length) break; var coords = this._getLatLngs(features[i]); if (coords) { var inside = this._pointInPolygon(p, coords); // NB. works only with polygons (see: https://observablehq.com/@tmcw/understanding-point-in-polygon). if (inside) results.push(features[i]); } } return results.length ? results : false; }, /** * (EXPERIMENTAL) Inspired by: https://github.com/Raruto/leaflet-pointable */ updateBalloon: function(e) { if (!this._map || !this.options.pointable || !this._map.isPointablePixel() || !this.isPointablePixel()) return; this._popup = this._popup || new L.Popup(); var points = this.pointInLayer(e.latlng, this.geojson); if (points) { var feature = points[0]; var name = feature.properties.name || ""; if (name) { this._popup.setLatLng(e.latlng); this._popup.setContent('<b>' + name + '</b>'); this._popup.openOn(this._map); } } else { this._map.closePopup(this._popup); } }, }); L.gridLayer.geoJson = function(geojson, options) { return new L.GridLayer.GeoJSON(geojson, options); }; var GridLayer = { GeoJSON: L.GridLayer.GeoJSON, }; var gridLayer = { geoJSON: L.gridLayer.geoJson, }; exports.GridLayer = GridLayer; exports.KMZLoader = KMZLoader; exports.KMZMarker = KMZMarker; exports.KMZParser = KMZParser; exports.gridLayer = gridLayer; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=leaflet-kmz-src.js.map