Merge pull request #579 from nylonee/master

Plugin to add a KML, GPX or GeoJSON overlay over the map
This commit is contained in:
Jon Atkins 2013-10-27 09:12:31 -07:00
commit 6833c02ff4
6 changed files with 799 additions and 0 deletions

View File

@ -10,5 +10,8 @@ This project is licensed under the permissive [ISC license](http://www.isc.org/d
- [taphold.js by Rich Adams; unknown](https://github.com/richadams/jquery-taphold)
- [L.Control.Pan.js by Kartena AB; same as Leaflet](https://github.com/kartena/Leaflet.Pancontrol)
- [L.Control.Zoomslider.js by Kartena AB; same as Leaflet](https://github.com/kartena/Leaflet.zoomslider)
- [KML.js by shramov; same as Leaflet](https://github.com/shramov/leaflet-plugins)
- [leaflet.filelayer.js by shramov; same as Leaflet](https://github.com/shramov/leaflet-plugins)
- [togeojson.js by shramov; same as Leaflet](https://github.com/shramov/leaflet-plugins)
- StackOverflow-CopyPasta is attributed in the source; [CC-Wiki](https://creativecommons.org/licenses/by-sa/3.0/)
- all Ingress/Niantic related stuff obviously remains non-free and is still copyrighted by Niantic/Google

336
external/KML.js vendored Executable file
View File

@ -0,0 +1,336 @@
/*global L: true */
L.KML = L.FeatureGroup.extend({
options: {
async: true
},
initialize: function(kml, options) {
L.Util.setOptions(this, options);
this._kml = kml;
this._layers = {};
if (kml) {
this.addKML(kml, options, this.options.async);
}
},
loadXML: function(url, cb, options, async) {
if (async == undefined) async = this.options.async;
if (options == undefined) options = this.options;
var req = new window.XMLHttpRequest();
req.open('GET', url, async);
try {
req.overrideMimeType('text/xml'); // unsupported by IE
} catch(e) {}
req.onreadystatechange = function() {
if (req.readyState != 4) return;
if(req.status == 200) cb(req.responseXML, options);
};
req.send(null);
},
addKML: function(url, options, async) {
var _this = this;
var cb = function(gpx, options) { _this._addKML(gpx, options) };
this.loadXML(url, cb, options, async);
},
_addKML: function(xml, options) {
var layers = L.KML.parseKML(xml);
if (!layers || !layers.length) return;
for (var i = 0; i < layers.length; i++)
{
this.fire('addlayer', {
layer: layers[i]
});
this.addLayer(layers[i]);
}
this.latLngs = L.KML.getLatLngs(xml);
this.fire("loaded");
},
latLngs: []
});
L.Util.extend(L.KML, {
parseKML: function (xml) {
var style = this.parseStyle(xml);
var el = xml.getElementsByTagName("Folder");
var layers = [], l;
for (var i = 0; i < el.length; i++) {
if (!this._check_folder(el[i])) { continue; }
l = this.parseFolder(el[i], style);
if (l) { layers.push(l); }
}
el = xml.getElementsByTagName('Placemark');
for (var j = 0; j < el.length; j++) {
if (!this._check_folder(el[j])) { continue; }
l = this.parsePlacemark(el[j], xml, style);
if (l) { layers.push(l); }
}
return layers;
},
// Return false if e's first parent Folder is not [folder]
// - returns true if no parent Folders
_check_folder: function (e, folder) {
e = e.parentElement;
while (e && e.tagName !== "Folder")
{
e = e.parentElement;
}
return !e || e === folder;
},
parseStyle: function (xml) {
var style = {};
var sl = xml.getElementsByTagName("Style");
//for (var i = 0; i < sl.length; i++) {
var attributes = {color: true, width: true, Icon: true, href: true,
hotSpot: true};
function _parse(xml) {
var options = {};
for (var i = 0; i < xml.childNodes.length; i++) {
var e = xml.childNodes[i];
var key = e.tagName;
if (!attributes[key]) { continue; }
if (key === 'hotSpot')
{
for (var j = 0; j < e.attributes.length; j++) {
options[e.attributes[j].name] = e.attributes[j].nodeValue;
}
} else {
var value = e.childNodes[0].nodeValue;
if (key === 'color') {
options.opacity = parseInt(value.substring(0, 2), 16) / 255.0;
options.color = "#" + value.substring(6, 8) + value.substring(4, 6) + value.substring(2, 4);
} else if (key === 'width') {
options.weight = value;
} else if (key === 'Icon') {
ioptions = _parse(e);
if (ioptions.href) { options.href = ioptions.href; }
} else if (key === 'href') {
options.href = value;
}
}
}
return options;
}
for (var i = 0; i < sl.length; i++) {
var e = sl[i], el;
var options = {}, poptions = {}, ioptions = {};
el = e.getElementsByTagName("LineStyle");
if (el && el[0]) { options = _parse(el[0]); }
el = e.getElementsByTagName("PolyStyle");
if (el && el[0]) { poptions = _parse(el[0]); }
if (poptions.color) { options.fillColor = poptions.color; }
if (poptions.opacity) { options.fillOpacity = poptions.opacity; }
el = e.getElementsByTagName("IconStyle");
if (el && el[0]) { ioptions = _parse(el[0]); }
if (ioptions.href) {
// save anchor info until the image is loaded
options.icon = new L.KMLIcon({
iconUrl: ioptions.href,
shadowUrl: null,
iconAnchorRef: {x: ioptions.x, y: ioptions.y},
iconAnchorType: {x: ioptions.xunits, y: ioptions.yunits}
});
}
style['#' + e.getAttribute('id')] = options;
}
return style;
},
parseFolder: function (xml, style) {
var el, layers = [], l;
el = xml.getElementsByTagName('Folder');
for (var i = 0; i < el.length; i++) {
if (!this._check_folder(el[i], xml)) { continue; }
l = this.parseFolder(el[i], style);
if (l) { layers.push(l); }
}
el = xml.getElementsByTagName('Placemark');
for (var j = 0; j < el.length; j++) {
if (!this._check_folder(el[j], xml)) { continue; }
l = this.parsePlacemark(el[j], xml, style);
if (l) { layers.push(l); }
}
if (!layers.length) { return; }
if (layers.length === 1) { return layers[0]; }
return new L.FeatureGroup(layers);
},
parsePlacemark: function (place, xml, style) {
var i, j, el, options = {};
el = place.getElementsByTagName('styleUrl');
for (i = 0; i < el.length; i++) {
var url = el[i].childNodes[0].nodeValue;
for (var a in style[url])
{
// for jshint
if (true)
{
options[a] = style[url][a];
}
}
}
var layers = [];
var parse = ['LineString', 'Polygon', 'Point'];
for (j in parse) {
// for jshint
if (true)
{
var tag = parse[j];
el = place.getElementsByTagName(tag);
for (i = 0; i < el.length; i++) {
var l = this["parse" + tag](el[i], xml, options);
if (l) { layers.push(l); }
}
}
}
if (!layers.length) {
return;
}
var layer = layers[0];
if (layers.length > 1) {
layer = new L.FeatureGroup(layers);
}
var name, descr = "";
el = place.getElementsByTagName('name');
if (el.length) {
name = el[0].childNodes[0].nodeValue;
}
el = place.getElementsByTagName('description');
for (i = 0; i < el.length; i++) {
for (j = 0; j < el[i].childNodes.length; j++) {
descr = descr + el[i].childNodes[j].nodeValue;
}
}
if (name) {
layer.bindPopup("<h2>" + name + "</h2>" + descr);
}
return layer;
},
parseCoords: function (xml) {
var el = xml.getElementsByTagName('coordinates');
return this._read_coords(el[0]);
},
parseLineString: function (line, xml, options) {
var coords = this.parseCoords(line);
if (!coords.length) { return; }
return new L.Polyline(coords, options);
},
parsePoint: function (line, xml, options) {
var el = line.getElementsByTagName('coordinates');
if (!el.length) {
return;
}
var ll = el[0].childNodes[0].nodeValue.split(',');
return new L.KMLMarker(new L.LatLng(ll[1], ll[0]), options);
},
parsePolygon: function (line, xml, options) {
var el, polys = [], inner = [], i, coords;
el = line.getElementsByTagName('outerBoundaryIs');
for (i = 0; i < el.length; i++) {
coords = this.parseCoords(el[i]);
if (coords) {
polys.push(coords);
}
}
el = line.getElementsByTagName('innerBoundaryIs');
for (i = 0; i < el.length; i++) {
coords = this.parseCoords(el[i]);
if (coords) {
inner.push(coords);
}
}
if (!polys.length) {
return;
}
if (options.fillColor) {
options.fill = true;
}
if (polys.length === 1) {
return new L.Polygon(polys.concat(inner), options);
}
return new L.MultiPolygon(polys, options);
},
getLatLngs: function (xml) {
var el = xml.getElementsByTagName('coordinates');
var coords = [];
for (var j = 0; j < el.length; j++) {
// text might span many childnodes
coords = coords.concat(this._read_coords(el[j]));
}
return coords;
},
_read_coords: function (el) {
var text = "", coords = [], i;
for (i = 0; i < el.childNodes.length; i++) {
text = text + el.childNodes[i].nodeValue;
}
text = text.split(/[\s\n]+/);
for (i = 0; i < text.length; i++) {
var ll = text[i].split(',');
if (ll.length < 2) {
continue;
}
coords.push(new L.LatLng(ll[1], ll[0]));
}
return coords;
}
});
L.KMLIcon = L.Icon.extend({
createIcon: function () {
var img = this._createIcon('icon');
img.onload = function () {
var i = new Image();
i.src = this.src;
this.style.width = i.width + 'px';
this.style.height = i.height + 'px';
if (this.anchorType.x === 'UNITS_FRACTION' || this.anchorType.x === 'fraction') {
img.style.marginLeft = (-this.anchor.x * i.width) + 'px';
}
if (this.anchorType.y === 'UNITS_FRACTION' || this.anchorType.x === 'fraction') {
img.style.marginTop = (-(1 - this.anchor.y) * i.height) + 'px';
}
this.style.display = "";
};
return img;
},
_setIconStyles: function (img, name) {
L.Icon.prototype._setIconStyles.apply(this, [img, name])
// save anchor information to the image
img.anchor = this.options.iconAnchorRef;
img.anchorType = this.options.iconAnchorType;
}
});
L.KMLMarker = L.Marker.extend({
options: {
icon: new L.KMLIcon.Default()
}
});

168
external/leaflet.filelayer.js vendored Executable file
View File

@ -0,0 +1,168 @@
/*
* Load files *locally* (GeoJSON, KML, GPX) into the map
* using the HTML5 File API.
*
* Requires Pavel Shramov's GPX.js
* https://github.com/shramov/leaflet-plugins/blob/d74d67/layer/vector/GPX.js
*/
var FileLoader = L.Class.extend({
includes: L.Mixin.Events,
options: {
layerOptions: {}
},
initialize: function (map, options) {
this._map = map;
L.Util.setOptions(this, options);
this._parsers = {
'geojson': this._loadGeoJSON,
'gpx': this._convertToGeoJSON,
'kml': this._convertToGeoJSON
};
},
load: function (file /* File */) {
// Check file extension
var ext = file.name.split('.').pop(),
parser = this._parsers[ext];
if (!parser) {
window.alert("Unsupported file type " + file.type + '(' + ext + ')');
return;
}
// Read selected file using HTML5 File API
var reader = new FileReader();
reader.onload = L.Util.bind(function (e) {
this.fire('data:loading', {filename: file.name, format: ext});
var layer = parser.call(this, e.target.result, ext);
this.fire('data:loaded', {layer: layer, filename: file.name, format: ext});
}, this);
reader.readAsText(file);
},
_loadGeoJSON: function (content) {
if (typeof content == 'string') {
content = JSON.parse(content);
}
return L.geoJson(content, this.options.layerOptions).addTo(this._map);
},
_convertToGeoJSON: function (content, format) {
// Format is either 'gpx' or 'kml'
if (typeof content == 'string') {
content = ( new window.DOMParser() ).parseFromString(content, "text/xml");
}
var geojson = toGeoJSON[format](content);
return this._loadGeoJSON(geojson);
}
});
L.Control.FileLayerLoad = L.Control.extend({
statics: {
TITLE: 'Load local file (GPX, KML, GeoJSON)',
LABEL: '&#8965;'
},
options: {
position: 'topleft',
fitBounds: true,
layerOptions: {}
},
initialize: function (options) {
L.Util.setOptions(this, options);
this.loader = null;
},
onAdd: function (map) {
this.loader = new FileLoader(map, {layerOptions: this.options.layerOptions});
this.loader.on('data:loaded', function (e) {
// Fit bounds after loading
if (this.options.fitBounds) {
window.setTimeout(function () {
map.fitBounds(e.layer.getBounds()).zoomOut();
}, 500);
}
}, this);
// Initialize Drag-and-drop
this._initDragAndDrop(map);
// Initialize map control
return this._initContainer();
},
_initDragAndDrop: function (map) {
var fileLoader = this.loader,
dropbox = map._container;
var callbacks = {
dragenter: function () {
map.scrollWheelZoom.disable();
},
dragleave: function () {
map.scrollWheelZoom.enable();
},
dragover: function (e) {
e.stopPropagation();
e.preventDefault();
},
drop: function (e) {
e.stopPropagation();
e.preventDefault();
var files = Array.prototype.slice.apply(e.dataTransfer.files),
i = files.length;
setTimeout(function(){
fileLoader.load(files.shift());
if (files.length > 0) {
setTimeout(arguments.callee, 25);
}
}, 25);
map.scrollWheelZoom.enable();
}
};
for (var name in callbacks)
dropbox.addEventListener(name, callbacks[name], false);
},
_initContainer: function () {
// Create an invisible file input
var fileInput = L.DomUtil.create('input', 'hidden', container);
fileInput.type = 'file';
fileInput.accept = '.gpx,.kml,.geojson';
fileInput.style.display = 'none';
// Load on file change
var fileLoader = this.loader;
fileInput.addEventListener("change", function (e) {
fileLoader.load(this.files[0]);
}, false);
// Create a button, and bind click on hidden file input
var zoomName = 'leaflet-control-filelayer leaflet-control-zoom',
barName = 'leaflet-bar',
partName = barName + '-part',
container = L.DomUtil.create('div', zoomName + ' ' + barName);
var link = L.DomUtil.create('a', zoomName + '-in ' + partName, container);
link.innerHTML = L.Control.FileLayerLoad.LABEL;
link.href = '#';
link.title = L.Control.FileLayerLoad.TITLE;
var stop = L.DomEvent.stopPropagation;
L.DomEvent
.on(link, 'click', stop)
.on(link, 'mousedown', stop)
.on(link, 'dblclick', stop)
.on(link, 'click', L.DomEvent.preventDefault)
.on(link, 'click', function (e) {
fileInput.click();
e.preventDefault();
});
return container;
}
});
L.Control.fileLayerLoad = function (options) {
return new L.Control.FileLayerLoad(options);
};

224
external/togeojson.js vendored Executable file
View File

@ -0,0 +1,224 @@
toGeoJSON = (function() {
'use strict';
var removeSpace = (/\s*/g),
trimSpace = (/^\s*|\s*$/g),
splitSpace = (/\s+/);
// generate a short, numeric hash of a string
function okhash(x) {
if (!x || !x.length) return 0;
for (var i = 0, h = 0; i < x.length; i++) {
h = ((h << 5) - h) + x.charCodeAt(i) | 0;
} return h;
}
// all Y children of X
function get(x, y) { return x.getElementsByTagName(y); }
function attr(x, y) { return x.getAttribute(y); }
function attrf(x, y) { return parseFloat(attr(x, y)); }
// one Y child of X, if any, otherwise null
function get1(x, y) { var n = get(x, y); return n.length ? n[0] : null; }
// https://developer.mozilla.org/en-US/docs/Web/API/Node.normalize
function norm(el) { if (el.normalize) { el.normalize(); } return el; }
// cast array x into numbers
function numarray(x) {
for (var j = 0, o = []; j < x.length; j++) o[j] = parseFloat(x[j]);
return o;
}
function clean(x) {
var o = {};
for (var i in x) if (x[i]) o[i] = x[i];
return o;
}
// get the content of a text node, if any
function nodeVal(x) { if (x) {norm(x);} return x && x.firstChild && x.firstChild.nodeValue; }
// get one coordinate from a coordinate array, if any
function coord1(v) { return numarray(v.replace(removeSpace, '').split(',')); }
// get all coordinates from a coordinate array as [[],[]]
function coord(v) {
var coords = v.replace(trimSpace, '').split(splitSpace),
o = [];
for (var i = 0; i < coords.length; i++) {
o.push(coord1(coords[i]));
}
return o;
}
function coordPair(x) { return [attrf(x, 'lon'), attrf(x, 'lat')]; }
// create a new feature collection parent object
function fc() {
return {
type: 'FeatureCollection',
features: []
};
}
var serializer;
if (typeof XMLSerializer !== 'undefined') {
serializer = new XMLSerializer();
} else if (typeof require !== 'undefined') {
serializer = new (require('xmldom').XMLSerializer)();
}
function xml2str(str) { return serializer.serializeToString(str); }
var t = {
kml: function(doc, o) {
o = o || {};
var gj = fc(),
// styleindex keeps track of hashed styles in order to match features
styleIndex = {},
// atomic geospatial types supported by KML - MultiGeometry is
// handled separately
geotypes = ['Polygon', 'LineString', 'Point', 'Track'],
// all root placemarks in the file
placemarks = get(doc, 'Placemark'),
styles = get(doc, 'Style');
for (var k = 0; k < styles.length; k++) {
styleIndex['#' + attr(styles[k], 'id')] = okhash(xml2str(styles[k])).toString(16);
}
for (var j = 0; j < placemarks.length; j++) {
gj.features = gj.features.concat(getPlacemark(placemarks[j]));
}
function gxCoord(v) { return numarray(v.split(' ')); }
function gxCoords(root) {
var elems = get(root, 'coord', 'gx'), coords = [];
for (var i = 0; i < elems.length; i++) coords.push(gxCoord(nodeVal(elems[i])));
return coords;
}
function getGeometry(root) {
var geomNode, geomNodes, i, j, k, geoms = [];
if (get1(root, 'MultiGeometry')) return getGeometry(get1(root, 'MultiGeometry'));
if (get1(root, 'MultiTrack')) return getGeometry(get1(root, 'MultiTrack'));
for (i = 0; i < geotypes.length; i++) {
geomNodes = get(root, geotypes[i]);
if (geomNodes) {
for (j = 0; j < geomNodes.length; j++) {
geomNode = geomNodes[j];
if (geotypes[i] == 'Point') {
geoms.push({
type: 'Point',
coordinates: coord1(nodeVal(get1(geomNode, 'coordinates')))
});
} else if (geotypes[i] == 'LineString') {
geoms.push({
type: 'LineString',
coordinates: coord(nodeVal(get1(geomNode, 'coordinates')))
});
} else if (geotypes[i] == 'Polygon') {
var rings = get(geomNode, 'LinearRing'),
coords = [];
for (k = 0; k < rings.length; k++) {
coords.push(coord(nodeVal(get1(rings[k], 'coordinates'))));
}
geoms.push({
type: 'Polygon',
coordinates: coords
});
} else if (geotypes[i] == 'Track') {
geoms.push({
type: 'LineString',
coordinates: gxCoords(geomNode)
});
}
}
}
}
return geoms;
}
function getPlacemark(root) {
var geoms = getGeometry(root), i, properties = {},
name = nodeVal(get1(root, 'name')),
styleUrl = nodeVal(get1(root, 'styleUrl')),
description = nodeVal(get1(root, 'description')),
extendedData = get1(root, 'ExtendedData');
if (!geoms.length) return [];
if (name) properties.name = name;
if (styleUrl && styleIndex[styleUrl]) {
properties.styleUrl = styleUrl;
properties.styleHash = styleIndex[styleUrl];
}
if (description) properties.description = description;
if (extendedData) {
var datas = get(extendedData, 'Data'),
simpleDatas = get(extendedData, 'SimpleData');
for (i = 0; i < datas.length; i++) {
properties[datas[i].getAttribute('name')] = nodeVal(get1(datas[i], 'value'));
}
for (i = 0; i < simpleDatas.length; i++) {
properties[simpleDatas[i].getAttribute('name')] = nodeVal(simpleDatas[i]);
}
}
return [{
type: 'Feature',
geometry: (geoms.length === 1) ? geoms[0] : {
type: 'GeometryCollection',
geometries: geoms
},
properties: properties
}];
}
return gj;
},
gpx: function(doc, o) {
var i,
tracks = get(doc, 'trk'),
routes = get(doc, 'rte'),
waypoints = get(doc, 'wpt'),
// a feature collection
gj = fc();
for (i = 0; i < tracks.length; i++) {
gj.features.push(getLinestring(tracks[i], 'trkpt'));
}
for (i = 0; i < routes.length; i++) {
gj.features.push(getLinestring(routes[i], 'rtept'));
}
for (i = 0; i < waypoints.length; i++) {
gj.features.push(getPoint(waypoints[i]));
}
function getLinestring(node, pointname) {
var j, pts = get(node, pointname), line = [];
for (j = 0; j < pts.length; j++) {
line.push(coordPair(pts[j]));
}
return {
type: 'Feature',
properties: getProperties(node),
geometry: {
type: 'LineString',
coordinates: line
}
};
}
function getPoint(node) {
var prop = getProperties(node);
prop.ele = nodeVal(get1(node, 'ele'));
prop.sym = nodeVal(get1(node, 'sym'));
return {
type: 'Feature',
properties: prop,
geometry: {
type: 'Point',
coordinates: coordPair(node)
}
};
}
function getProperties(node) {
var meta = ['name', 'desc', 'author', 'copyright', 'link',
'time', 'keywords'],
prop = {},
k;
for (k = 0; k < meta.length; k++) {
prop[meta[k]] = nodeVal(get1(node, meta[k]));
}
return clean(prop);
}
return gj;
}
};
return t;
})();
if (typeof module !== 'undefined') module.exports = toGeoJSON;

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

68
plugins/add-kml.user.js Executable file
View File

@ -0,0 +1,68 @@
// ==UserScript==
// @id overlay-kml@danielatkins
// @name IITC plugin: overlay KML
// @category Info
// @version 0.1.@@DATETIMEVERSION@@
// @namespace https://github.com/jonatkins/ingress-intel-total-conversion
// @updateURL @@UPDATEURL@@
// @downloadURL @@DOWNLOADURL@@
// @description [@@BUILDNAME@@-@@BUILDDATE@@] Allows users to overlay their own KML / GPX files on top of IITC
// @include https://www.ingress.com/intel*
// @include http://www.ingress.com/intel*
// @match https://www.ingress.com/intel*
// @match http://www.ingress.com/intel*
// @grant none
// ==/UserScript==
@@PLUGINSTART@@
// PLUGIN START ////////////////////////////////////////////////////////
// use own namespace for plugin
window.plugin.overlayKML = function() {};
window.plugin.overlayKML.loadExternals = function() {
try { console.log('Loading leaflet.filelayer JS now'); } catch(e) {}
@@INCLUDERAW:external/leaflet.filelayer.js@@
try { console.log('done loading leaflet.filelayer JS'); } catch(e) {}
try { console.log('Loading KML JS now'); } catch(e) {}
@@INCLUDERAW:external/KML.js@@
try { console.log('done loading KML JS'); } catch(e) {}
try { console.log('Loading togeojson JS now'); } catch(e) {}
@@INCLUDERAW:external/togeojson.js@@
try { console.log('done loading togeojson JS'); } catch(e) {}
window.plugin.overlayKML.load();
}
window.plugin.overlayKML.load = function() {
// Provide popup window allow user to select KML to overlay
L.Icon.Default.imagePath = '@@INCLUDEIMAGE:images/marker-icon.png@@';
var KMLIcon = L.icon({
iconUrl: '@@INCLUDEIMAGE:images/marker-icon.png@@',
iconSize: [16, 24], // size of the icon
iconAnchor: [8, 24], // point of the icon which will correspond to marker's location
popupAnchor: [-3, 16] // point from which the popup should open relative to the iconAnchor
});
L.Control.FileLayerLoad.LABEL = '<img src="@@INCLUDEIMAGE:images/open-folder-icon_sml.png@@" alt="Open" />';
L.Control.fileLayerLoad({
fitBounds: true,
layerOptions: {
pointToLayer: function (data, latlng) {
return L.marker(latlng, {icon: KMLIcon});
}},
}).addTo(map);
}
var setup = function() {
window.plugin.overlayKML.loadExternals();
}
// PLUGIN END //////////////////////////////////////////////////////////
@@PLUGINEND@@