node-red-contrib-web-worldmap
Version:
A Node-RED node to provide a web page of a world map for plotting things on.
1,193 lines (1,088 loc) • 94.7 kB
JavaScript
/* eslint-disable no-undef */
var startpos = [51.03, -1.379]; // Start location - somewhere in UK :-)
var startzoom = 10;
var ws;
var map;
var allData = {};
var markers = {};
var polygons = {};
var layers = {};
var overlays = {};
var basemaps = {};
var marks = [];
var buttons = {};
var marksIndex = 0;
var menuOpen = false;
var clusterAt = 0;
var maxage = 600; // default max age of icons on map in seconds - cleared after 10 mins
var baselayername = "OSM grey"; // Default base layer OSM but uniform grey
var ibmfoot = " © IBM 2015,2019"
var inIframe = false;
var showUserMenu = true;
var showLayerMenu = true;
var showMouseCoords = false;
var sidebyside;
var layercontrol;
var drawingColour = "#910000";
var iconSz = {
"Team/Crew": 24,
"Squad": 24,
"Section": 24,
"Platoon/detachment": 26,
"Company/battery/troop": 28,
"Battalion/squadron": 30,
"Regiment/group": 32,
"Brigade": 34,
"Division": 36,
"Corps/MEF": 36,
"Army": 40,
"Army Group/front": 40,
"Region/Theater": 44,
"Command": 44
};
// Polyfill assign for IE11 for now
if (typeof Object.assign !== 'function') {
// Must be writable: true, enumerable: false, configurable: true
Object.defineProperty(Object, "assign", {
value: function assign(target, varArgs) { // .length of function is 2
'use strict';
if (target === null || target === undefined) {
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource !== null && nextSource !== undefined) {
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
// Create the socket
var connect = function() {
ws = new SockJS(location.pathname.split("index")[0] + 'socket');
ws.onopen = function() {
console.log("CONNECTED");
if (!inIframe) {
document.getElementById("footer").innerHTML = "<font color='#494'>"+ibmfoot+"</font>";
}
ws.send(JSON.stringify({action:"connected"}));
onoffline();
};
ws.onclose = function() {
console.log("DISCONNECTED");
if (!inIframe) {
document.getElementById("footer").innerHTML = "<font color='#900'>"+ibmfoot+"</font>";
}
setTimeout(function() { connect(); }, 2500);
};
ws.onmessage = function(e) {
var data = JSON.parse(e.data);
//console.log("DATA" typeof data,data);
if (Array.isArray(data)) {
//console.log("ARRAY");
// map.closePopup();
// var bnds= L.latLngBounds([0,0]);
for (var prop in data) {
if (data[prop].command) { doCommand(data[prop].command); delete data[prop].command; }
if (data[prop].hasOwnProperty("name")) {
setMarker(data[prop]);
// bnds.extend(markers[data[prop].name].getLatLng());
}
else { console.log("SKIP A",data[prop]); }
}
// map.fitBounds(bnds.pad(0.25));
}
else {
if (data.command) { doCommand(data.command); delete data.command; }
if (data.hasOwnProperty("name")) { setMarker(data); }
else if (data.hasOwnProperty("type")) { doGeojson(data); }
else {
console.log("SKIP",data);
// if (typeof data === "string") { doDialog(data); }
// else { console.log("SKIP",data); }
}
}
};
}
console.log("CONNECT TO",location.pathname + 'socket');
window.onunload = function() { if (ws) ws.close(); }
var onoffline = function() { if (!navigator.onLine) map.addLayer(layers["_countries"]); }
// Set Ctl-Alt-3 to switch to 3d view
document.addEventListener ("keydown", function (ev) {
if (ev.ctrlKey && ev.altKey && ev.code === "Digit3") {
ws.close();
window.location.href = "index3d.html";
}
});
// Create the Initial Map object.
map = new L.map('map').setView(startpos, startzoom);
// Create some buttons
var menuButton = L.easyButton({states:[{icon:'fa-bars fa-lg', onClick:function() { toggleMenu(); }, title:'Toggle menu'}], position:"topright"});
var fullscreenButton = L.control.fullscreen();
var rulerButton = L.control.ruler({position:"topleft"});
//var colorPickButton = L.easyButton({states:[{icon:'fa-tint fa-lg', onClick:function() { console.log("PICK"); }, title:'Pick Colour'}]});
var redButton = L.easyButton('fa-square wm-red', function(btn) { changeDrawColour("#E7827F"); })
var blueButton = L.easyButton('fa-square wm-blue', function(btn) { changeDrawColour("#94CCE2"); })
var greenButton = L.easyButton('fa-square wm-green', function(btn) { changeDrawColour("#ACD6A4"); })
var yellowButton = L.easyButton('fa-square wm-yellow', function(btn) { changeDrawColour("#F5F08B"); })
var blackButton = L.easyButton('fa-square wm-black', function(btn) { changeDrawColour("#444444"); })
var whiteButton = L.easyButton('fa-square wm-white', function(btn) { changeDrawColour("#EEEEEE"); })
var colorControl = L.easyBar([redButton,blueButton,greenButton,yellowButton,blackButton,whiteButton]);
function onLocationFound(e) {
var radius = e.accuracy;
//L.marker(e.latlng).addTo(map).bindPopup("You are within " + radius + " meters from this point").openPopup();
L.circle(e.latlng, radius, {color:"cyan", weight:4, opacity:0.8, fill:false, clickable:false}).addTo(map);
if (e.hasOwnProperty("heading")) {
var lengthAsDegrees = e.speed * 60 / 110540;
var ya = e.latlng.lat + Math.sin((90-e.heading)/180*Math.PI)*lengthAsDegrees*Math.cos(e.latlng.lng/180*Math.PI);
var xa = e.latlng.lng + Math.cos((90-e.heading)/180*Math.PI)*lengthAsDegrees;
var lla = new L.LatLng(ya,xa);
L.polygon([ e.latlng, lla ], {color:"cyan", weight:3, opacity:0.8, clickable:false}).addTo(map);
}
ws.send(JSON.stringify({action:"point", lat:e.latlng.lat.toFixed(5), lon:e.latlng.lng.toFixed(5), point:"self", bearing:e.heading, speed:(e.speed*3.6 || undefined)}));
}
function onLocationError(e) { console.log(e.message); }
// Move some bits around if in an iframe
if (window.self !== window.top) {
console.log("IN an iframe");
inIframe = true;
if (showUserMenu) { menuButton.addTo(map); }
document.getElementById("topbar").style.display="none";
document.getElementById("map").style.top="0px";
document.getElementById("results").style.right="50px";
document.getElementById("results").style.top="10px";
document.getElementById("results").style.zIndex="1";
document.getElementById("results").style.height="31px";
document.getElementById("results").style.paddingTop="6px";
document.getElementById("bars").style.display="none";
document.getElementById("menu").style.right="8px";
document.getElementById("menu").style.borderRadius="6px";
}
else {
console.log("NOT in an iframe");
if (!showUserMenu) { document.getElementById("bars").style.display="none"; }
// Add the fullscreen button
fullscreenButton.addTo(map);
// Add the locate my position button
L.easyButton( 'fa-crosshairs fa-lg', function() {
map.locate({setView:true, maxZoom:16});
}, "Locate me").addTo(map);
map.on('locationfound', onLocationFound);
map.on('locationerror', onLocationError);
// Add the measure/ruler button
rulerButton.addTo(map);
// Create the clear heatmap button
var clrHeat = L.easyButton( '<b>Reset Heatmap</b>', function() {
console.log("Reset heatmap");
heat.setLatLngs([]);
}, "Clears the current heatmap", "bottomright");
}
var helpMenu = '<table>'
helpMenu += '<tr><td><input type="text" name="search" id="search" size="20" style="width:150px;"/> <span onclick=\'doSearch();\'><i class="fa fa-search fa-lg"></i></span></td></tr>';
helpMenu += '<tr><td style="cursor:default"><i class="fa fa-spinner fa-lg fa-fw"></i> Set Max Age <input type="text" name="maxage" id="maxage" value="600" size="5" onchange=\'setMaxAge();\'/>s</td></tr>';
helpMenu += '<tr><td style="cursor:default"><i class="fa fa-search-plus fa-lg fa-fw"></i> Cluster at zoom <<input type="text" name="setclus" id="setclus" size="2" onchange=\'setCluster(this.value);\'/></td></tr>';
helpMenu += '<tr><td style="cursor:default"><input type="checkbox" id="panit" onclick=\'doPanit(this.checked);\'/> Auto Pan Map</td></tr>';
helpMenu += '<tr><td style="cursor:default"><input type="checkbox" id="lockit" onclick=\'doLock(this.checked);\'/> Lock Map</td></tr>';
helpMenu += '<tr><td style="cursor:default"><input type="checkbox" id="heatall" onclick=\'doHeatAll(this.checked);\'/> Heatmap all layers</td></tr>';
if (!inIframe) { helpMenu += '<tr><td style="cursor:default"><span id="showHelp" onclick=\'doDialog(helpText);\'><i class="fa fa-info fa-lg fa-fw"></i>Help</span></td></tr></table>'; }
else { helpMenu += '</table>' }
document.getElementById('menu').innerHTML = helpMenu;
if (showUserMenu) {
if ( window.localStorage.hasOwnProperty("lastpos") ) {
var sp = JSON.parse(window.localStorage.getItem("lastpos"));
startpos = [ sp.lat, sp.lng ];
}
if ( window.localStorage.hasOwnProperty("lastzoom") ) {
startzoom = window.localStorage.getItem("lastzoom");
}
// if ( window.localStorage.hasOwnProperty("clusterat") ) {
// clusterAt = window.localStorage.getItem("clusterat");
// document.getElementById("setclus").value = clusterAt;
// }
if ( window.localStorage.hasOwnProperty("maxage") ) {
maxage = window.localStorage.getItem("maxage");
document.getElementById("maxage").value = maxage;
}
}
// Add graticule
var showGrid = false;
var Lgrid = L.latlngGraticule({
font: "Verdana",
fontColor: "#666",
zoomInterval: [
{start:1, end:2, interval:40},
{start:3, end:3, interval:20},
{start:4, end:4, interval:10},
{start:5, end:7, interval:5},
{start:8, end:20, interval:1}
]
});
var panit = false;
function doPanit(v) {
if (v !== undefined) { panit = v; }
console.log("Panit set :",panit);
}
var heatAll = false;
function doHeatAll(v) {
if (v !== undefined) { heatall = v; }
console.log("Heatall set :",heatAll);
}
var lockit = false;
var mb = new L.LatLngBounds([[-120,-360],[120,360]]);
function doLock(v) {
if (v !== undefined) { lockit = v; }
if (lockit === false) {
mb = new L.LatLngBounds([[-120,-360],[120,360]]);
map.dragging.enable();
}
else {
mb = map.getBounds();
map.dragging.disable();
window.localStorage.setItem("lastpos",JSON.stringify(map.getCenter()));
window.localStorage.setItem("lastzoom", map.getZoom());
window.localStorage.setItem("lastlayer", baselayername);
//window.localStorage.setItem("clusterat", clusterAt);
window.localStorage.setItem("maxage", maxage);
console.log("Saved :",JSON.stringify(map.getCenter()),map.getZoom(),baselayername);
}
map.setMaxBounds(mb);
//console.log("Map bounds lock :",lockit);
}
// Remove old markers
function doTidyUp(l) {
var d = parseInt(Date.now()/1000);
for (var m in markers) {
if ((l && (l == markers[m].lay)) || typeof markers[m].ts != "undefined") {
if ((l && (l == markers[m].lay)) || (markers[m].hasOwnProperty("ts") && (Number(markers[m].ts) < d) && (markers[m].lay !== "_drawing"))) {
//console.log("STALE :",m);
if (typeof polygons[m+"_"] != "undefined") {
layers[polygons[m+"_"].lay].removeLayer(polygons[m+"_"]);
delete polygons[m+"_"];
}
if (typeof polygons[m] != "undefined") {
layers[markers[m].lay].removeLayer(polygons[m]);
delete polygons[m];
}
layers[markers[m].lay].removeLayer(markers[m]);
delete markers[m];
}
}
}
if (l) {
if (layers[l]) { map.removeLayer(layers[l]); layercontrol.removeLayer(layers[l]); delete layers[l]; }
if (overlays[l]) { map.removeLayer(overlays[l]); layercontrol.removeLayer(overlays[l]); delete overlays[l]; }
}
}
// Call tidyup every {maxage} seconds - default 10 mins
var stale = null;
function setMaxAge() {
maxage = document.getElementById('maxage').value;
if (stale) { clearInterval(stale); }
//if (maxage > 0) {
stale = setInterval( function() { doTidyUp() }, 20000); // check every 20 secs
//} //every minute
//console.log("Stale time set :",maxage+"s");
}
setMaxAge();
// move the daylight / nighttime boundary (if enabled) every minute
function moveTerminator() { // if terminator line plotted move it every minute
if (layers["_daynight"].getLayers().length > 0) {
layers["_daynight"].clearLayers();
layers["_daynight"].addLayer(L.terminator());
}
}
setInterval( function() { moveTerminator() }, 60000 );
function setCluster(v) {
clusterAt = v || 0;
console.log("clusterAt set:",clusterAt);
showMapCurrentZoom();
}
// Search for markers with names of ... or icons of ...
function doSearch() {
var value = document.getElementById('search').value;
marks = [];
marksIndex = 0;
for (var key in markers) {
if ( (~(key.toLowerCase()).indexOf(value.toLowerCase())) && (mb.contains(markers[key].getLatLng()))) {
marks.push(markers[key]);
}
if (markers[key].icon === value) {
marks.push(markers[key]);
}
}
moveToMarks();
if (marks.length === 0) {
// If no markers found let's try a geolookup...
var protocol = location.protocol;
if (protocol == "file:") { protocol = "https:"; }
var searchUrl = protocol + "//nominatim.openstreetmap.org/search?format=json&limit=1&q=";
fetch(searchUrl + value) // Call the fetch function passing the url of the API as a parameter
.then(function(resp) { return resp.json(); })
.then(function(data) {
if (data.length > 0) {
var bb = data[0].boundingbox;
map.fitBounds([ [bb[0],bb[2]], [bb[1],bb[3]] ]);
map.panTo([data[0].lat, data[0].lon]);
}
else {
document.getElementById('searchResult').innerHTML = " <font color='#ff0'>Not Found</font>";
}
})
.catch(function(err) {
if (err.toString() === "TypeError: Failed to fetch") {
document.getElementById('searchResult').innerHTML = " <font color='#ff0'>Not Found</font>";
}
});
}
else {
if (lockit) {
document.getElementById('searchResult').innerHTML = " <font color='#ff0'>Found "+marks.length+" results within bounds.</font>";
} else {
document.getElementById('searchResult').innerHTML = " <font color='#ff0'>Found "+marks.length+" results.</font>";
}
}
}
// Jump to a markers position - centralise it on map
function moveToMarks() {
if (marks.length > marksIndex) {
var m = marks[marksIndex];
map.setView(m.getLatLng(), map.getZoom());
m.openPopup();
marksIndex++;
setTimeout(moveToMarks, 2500);
}
}
// Clear Search With Marker names
function clearSearch() {
var value = document.getElementById('search').value;
marks = [];
marksIndex = 0;
for (var key in markers) {
if ( (~(key.toLowerCase()).indexOf(value.toLowerCase())) && (mb.contains(markers[key].getLatLng()))) {
marks.push(markers[key]);
}
}
removeMarks();
if (lockit) {
document.getElementById('searchResult').innerHTML = "";
}
else {
document.getElementById('searchResult').innerHTML = "";
}
}
function removeMarks() {
if (marks.length > marksIndex) {
var m = marks[marksIndex];
map.setView(m.getLatLng(), map.getZoom());
m.closePopup();
marksIndex++;
}
}
function toggleMenu() {
menuOpen = !menuOpen;
if (menuOpen) {
document.getElementById("menu").style.display = 'block';
} else {
document.getElementById("menu").style.display = 'none';
dialogue.close();
}
}
function openMenu() {
if (!menuOpen) {
menuOpen = true;
document.getElementById("menu").style.display = 'block';
}
}
function closeMenu() {
if (menuOpen) {
menuOpen = false;
document.getElementById("menu").style.display = 'none';
}
dialogue.close();
}
document.getElementById("menu").style.display = 'none';
map.on('overlayadd', function(e) {
if (typeof overlays[e.name].bringToFront === "function") {
overlays[e.name].bringToFront();
}
if (e.name == "satellite") {
overlays["satellite"].bringToBack();
}
if (e.name == "countries") {
overlays["countries"].bringToBack();
}
if (e.name == "heatmap") { // show heatmap button when it's layer is added.
clrHeat.addTo(map);
}
if (e.name == "day/night") {
layers["_daynight"].addLayer(L.terminator());
}
if (e.name == "drawing") {
overlays["drawing"].bringToFront();
map.addControl(drawControl);
map.addControl(colorControl);
}
ws.send(JSON.stringify({action:"addlayer", name:e.name}));
});
map.on('overlayremove', function(e) {
if (e.name == "heatmap") { // hide heatmap button when it's layer is removed.
clrHeat.removeFrom(map);
}
if (e.name == "day/night") {
layers["_daynight"].clearLayers();
}
if (e.name == "drawing") {
map.removeControl(colorControl);
map.removeControl(drawControl);
}
ws.send(JSON.stringify({action:"dellayer", name:e.name}));
});
map.on('baselayerchange', function(e) {
//console.log("base layer now :",e.name);
baselayername = e.name;
ws.send(JSON.stringify({action:"layer", name:e.name}));
});
function showMapCurrentZoom() {
console.log("zoom:",map.getZoom(),". clusterAt:",clusterAt);
for (var l in layers) {
if (layers[l].hasOwnProperty("_zoom")) {
if (map.getZoom() >= clusterAt) {
layers[l].disableClustering();
}
else {
layers[l].enableClustering();
}
}
}
setTimeout( function() {
for (var key in markers) {
if (polygons[key]) {
if (typeof layers[markers[key].lay].getVisibleParent === 'function') {
var vis = layers[markers[key].lay].getVisibleParent(markers[key]);
if ((vis) && (vis.hasOwnProperty("lay"))) {
polygons[key].setStyle({opacity:1});
}
else {
polygons[key].setStyle({opacity:0});
}
}
try {
if (polygons[key].hasOwnProperty("_layers")) {
polygons[key].eachLayer(function(layer) { layer.redraw(); });
}
else {
polygons[key].redraw();
}
} catch(e) {
console.log(key,polygons[key],e)
}
}
}
},750);
}
map.on('zoomend', function() {
showMapCurrentZoom();
});
//map.on('contextmenu', function(e) {
// ws.send(JSON.stringify({action:"rightclick", lat:e.latlng.lat.toFixed(5), lon:e.latlng.lng.toFixed(5)}));
//});
// single right click to add a marker
var addmenu = "<b>Add marker</b><br><input type='text' id='rinput' autofocus onkeydown='if (event.keyCode == 13) addThing();' placeholder='name (,icon, layer, colour)'/>";
var rightmenuMap = L.popup({keepInView:true, minWidth:250}).setContent(addmenu);
var rclk;
var hiderightclick = false;
var addThing = function() {
var thing = document.getElementById('rinput').value;
map.closePopup();
//popped = false;
var bits = thing.split(",");
var icon = (bits[1] || "circle").trim();
var lay = (bits[2] || "_drawing").trim();
var colo = (bits[3] || "#910000").trim();
var drag = true;
var regi = /^[S,G,E,I,O][A-Z]{4}.*/i; // if it looks like a SIDC code
var d = {action:"point", name:bits[0].trim(), layer:lay, draggable:drag, lat:rclk.lat, lon:rclk.lng};
if (regi.test(icon)) {
d.SIDC = (icon.toUpperCase()+"------------").substr(0,12);
}
else {
d.icon = icon;
d.iconColor = colo;
}
ws.send(JSON.stringify(d));
delete d.action;
setMarker(d);
map.addLayer(layers[lay]);
}
var feedback = function(n,v,a) {
ws.send(JSON.stringify({action:a||"feedback", name:n, value:v}));
}
// allow double right click to zoom out (if enabled)
// single right click opens a message window that adds a marker
var rclicked = false;
var rtout = null;
map.on('contextmenu', function(e) {
if (rclicked) {
rclicked = false;
clearTimeout(rtout);
if (map.doubleClickZoom.enabled()) {
map.zoomOut();
}
}
else {
rclicked = true;
rtout = setTimeout( function() {
rclicked = false;
if ((hiderightclick !== true) && (addmenu.length > 0)) {
rclk = e.latlng;
rightmenuMap.setLatLng(e.latlng);
map.openPopup(rightmenuMap);
setTimeout( function() {
document.getElementById('rinput').focus();
}, 200);
}
}, 300);
}
});
// Add all the base layer maps
// Use this for OSM online maps
var osmUrl='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
//var osmUrl='https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png';
var osmAttrib='Map data © OpenStreetMap contributors';
var osmg = new L.TileLayer.Grayscale(osmUrl, {attribution:osmAttrib, maxNativeZoom:19, maxZoom:20});
basemaps["OSM grey"] = osmg;
var osm = new L.TileLayer(osmUrl, {attribution:osmAttrib, maxNativeZoom:19, maxZoom:20});
basemaps["OSM"] = osm;
// Extra Leaflet map layers from https://leaflet-extras.github.io/leaflet-providers/preview/
var Esri_WorldStreetMap = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri', maxNativeZoom:19, maxZoom:20
});
basemaps["Esri"] = Esri_WorldStreetMap;
var Esri_WorldImagery = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution:'Tiles © Esri', maxNativeZoom:17, maxZoom:20
});
basemaps["Esri Satellite"] = Esri_WorldImagery;
var Esri_WorldTopoMap = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri — Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community'
});
basemaps["Esri Topography"] = Esri_WorldTopoMap;
// var Esri_WorldShadedRelief = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/{z}/{y}/{x}', {
// attribution: 'Tiles © Esri',
// maxNativeZoom:13
// });
// basemaps["Esri Terrain"] = Esri_WorldShadedRelief;
var Esri_OceanBasemap = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri — Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri',
maxZoom: 13
});
basemaps["Esri Ocean"] = Esri_OceanBasemap;
var Esri_WorldGrayCanvas = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri — Esri, DeLorme, NAVTEQ',
maxZoom: 16
});
basemaps["Esri Dark Grey"] = Esri_WorldGrayCanvas;
// var OpenMapSurfer_Roads = L.tileLayer('https://korona.geog.uni-heidelberg.de/tiles/roads/x={x}&y={y}&z={z}', {
// maxZoom: 18,
// attribution: 'Imagery from <a href="https://giscience.uni-hd.de/">University of Heidelberg</a> — Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
// });
// basemaps["Mapsurfer"] = OpenMapSurfer_Roads;
// var MapQuestOpen_OSM = L.tileLayer('https://otile{s}.mqcdn.com/tiles/1.0.0/{type}/{z}/{x}/{y}.{ext}', {
// type: 'map',
// ext: 'jpg',
// attribution: 'Tiles Courtesy of <a href="https://www.mapquest.com/">MapQuest</a> — Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
// subdomains: '1234',
// maxNativeZoom: 17
// });
//basemaps["MapQuest OSM"] = MapQuestOpen_OSM;
var Esri_NatGeoWorldMap = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri',
maxNativeZoom:12
});
basemaps["Nat Geo"] = Esri_NatGeoWorldMap;
var NLS_OS_opendata = L.tileLayer('https://geo.nls.uk/maps/opendata/{z}/{x}/{y}.png', {
attribution: '<a href="https://geo.nls.uk/maps/">National Library of Scotland Historic Maps</a>',
bounds: [[49.6, -12], [61.7, 3]],
minZoom:1, maxNativeZoom:18, maxZoom:18,
subdomains: '0123'
});
basemaps["UK OS Opendata"] = NLS_OS_opendata;
var HikeBike_HikeBike = L.tileLayer('https://tiles.wmflabs.org/hikebike/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
basemaps["Hike Bike"] = HikeBike_HikeBike;
var NLS_OS_1919_1947 = L.tileLayer( 'https://nls-{s}.tileserver.com/nls/{z}/{x}/{y}.jpg', {
attribution: 'Historical Maps Layer, from <a href="https://maps.nls.uk/projects/api/">NLS Maps</a>',
bounds: [[49.6, -12], [61.7, 3]],
minZoom:1, maxZoom:18,
subdomains: '0123'
});
basemaps["UK OS 1919-47"] = NLS_OS_1919_1947;
//var NLS_OS_1900 = L.tileLayer('https://nls-{s}.tileserver.com/NLS_API/{z}/{x}/{y}.jpg', {
var NLS_OS_1900 = L.tileLayer('https://nls-{s}.tileserver.com/fpsUZbzrfb5d/{z}/{x}/{y}.jpg', {
attribution: '<a href="https://geo.nls.uk/maps/">National Library of Scotland Historic Maps</a>',
bounds: [[49.6, -12], [61.7, 3]],
minZoom:1, maxNativeZoom:19, maxZoom:20,
subdomains: '0123'
});
basemaps["UK OS 1900"] = NLS_OS_1900;
//var CartoPos = L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png', {
// attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="https://cartodb.com/attributions">CartoDB</a>'
//});
//basemaps["CartoDB Light"] = CartoPos;
// Nice terrain based maps by Stamen Design
var terrainUrl = "https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}.jpg";
basemaps["Terrain"] = L.tileLayer(terrainUrl, {
subdomains: ['a','b','c','d'],
minZoom: 0,
maxZoom: 20,
type: 'jpg',
attribution: 'Map tiles by <a href="https://stamen.com">Stamen Design</a>, under <a href="https://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="https://openstreetmap.org">OpenStreetMap</a>, under <a href="https://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>'
});
// Nice watercolour based maps by Stamen Design
var watercolorUrl = "https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg";
basemaps["Watercolor"] = L.tileLayer(watercolorUrl, {
subdomains: ['a','b','c','d'],
minZoom: 0,
maxZoom: 20,
type: 'jpg',
attribution: 'Map tiles by <a href="https://stamen.com">Stamen Design</a>, under <a href="https://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="https://openstreetmap.org">OpenStreetMap</a>, under <a href="https://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>'
});
// Now add the overlays
// Add the countries (world-110m) for offline use
var customTopoLayer = L.geoJson(null, {clickable:false, style: {color:"blue", weight:2, fillColor:"#cf6", fillOpacity:0.04}});
layers["_countries"] = omnivore.topojson('images/world-50m-flat.json',null,customTopoLayer);
overlays["countries"] = layers["_countries"];
// Add the day/night overlay
layers["_daynight"] = new L.LayerGroup();
overlays["day/night"] = layers["_daynight"];
// Add the drawing layer for fun...
layers["_drawing"] = new L.FeatureGroup();
overlays["drawing"] = layers["_drawing"];
map.options.drawControlTooltips = false;
var drawCount = 0;
var drawControl = new L.Control.Draw({
draw: {
polyline: { shapeOptions: { clickable:true } },
marker: false,
//circle: false,
circle: { shapeOptions: { clickable:true } },
circlemarker: false,
rectangle: { shapeOptions: { clickable:true } },
polygon: { shapeOptions: { clickable:true } }
},
edit: false
// {
// featureGroup: layers["_drawing"],
// remove: true,
// edit: true
// }
});
var changeDrawColour = function(col) {
drawControl.setDrawingOptions({
polyline: { shapeOptions: { color:col } },
circle: { shapeOptions: { color:col } },
rectangle: { shapeOptions: { color:col } },
polygon: { shapeOptions: { color:col } }
});
}
map.on('draw:created', function (e) {
var name = e.layerType + drawCount;
drawCount = drawCount + 1;
var rightmenuMarker = L.popup({offset:[0,-12]}).setContent("<b>"+name+"</b><br/><button onclick='editPoly(\""+name+"\",true);'>Edit</button><button onclick='delMarker(\""+name+"\",true);'>Delete</button>");
e.layer.on('contextmenu', function(e) {
L.DomEvent.stopPropagation(e);
rightmenuMarker.setLatLng(e.latlng);
map.openPopup(rightmenuMarker);
});
var la, lo;
if (e.layer.hasOwnProperty("_latlng")) {
la = e.layer._latlng.lat;
lo = e.layer._latlng.lng;
}
var m = {action:"draw", name:name, layer:"_drawing", options:e.layer.options, radius:e.layer._mRadius, lat:la, lon:lo};
if (e.layer.hasOwnProperty("_latlngs")) {
if (e.layer.options.fill === false) { m.line = e.layer._latlngs; }
else { m.area = e.layer._latlngs[0]; }
}
ws.send(JSON.stringify(m));
polygons[name] = e.layer;
polygons[name].lay = "_drawing";
layers["_drawing"].addLayer(e.layer);
});
// Add the heatmap layer
var heat = L.heatLayer([], {radius:60, gradient:{0.2:'blue', 0.4:'lime', 0.6:'red', 0.8:'yellow', 1:'white'}});
layers["_heat"] = new L.LayerGroup().addLayer(heat);
overlays["heatmap"] = layers["_heat"];
// Add the buildings layer
overlays["buildings"] = new OSMBuildings(map).load();
map.removeLayer(overlays["buildings"]); // Hide it at start
// Add Roads
overlays["roads"] = L.tileLayer('https://{s}.tile.openstreetmap.se/hydda/roads_and_labels/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: 'Tiles courtesy of <a href="https://openstreetmap.se/" target="_blank">OpenStreetMap Sweden</a> — Map data © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
opacity: 0.8
});
// Add Railways
overlays["railways"] = L.tileLayer('https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: 'Map data: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> | Map style: © <a href="https://www.OpenRailwayMap.org">OpenRailwayMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)'
});
// Add Public Transport (Buses)
overlays["public transport"] = L.tileLayer('https://openptmap.org/tiles/{z}/{x}/{y}.png', {
maxZoom: 17,
attribution: 'Map data: © <a href="https://www.openptmap.org">OpenPtMap</a> contributors'
});
// Add the OpenSea markers layer
overlays["ship nav"] = L.tileLayer('https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: 'Map data: © <a href="https://www.openseamap.org">OpenSeaMap</a> contributors'
});
if (showUserMenu) {
if ( window.localStorage.hasOwnProperty("lastlayer") ) {
if ( basemaps[window.localStorage.getItem("lastlayer")] ) {
baselayername = window.localStorage.getItem("lastlayer");
}
}
}
basemaps[baselayername].addTo(map);
// Layer control based on select box rather than radio buttons.
//var layercontrol = L.control.selectLayers(basemaps, overlays).addTo(map);
layercontrol = L.control.layers(basemaps, overlays);
// Add the layers control widget
if (!inIframe) { layercontrol.addTo(map); }
else { showLayerMenu = false;}
var coords = L.control.coordinates({
position:"bottomleft", //optional default "bottomright"
decimals:4, //optional default 4
decimalSeperator:".", //optional default "."
labelTemplateLat:" Lat: {y}", //optional default "Lat: {y}"
labelTemplateLng:" Lon: {x}", //optional default "Lng: {x}"
enableUserInput:false, //optional default true
useDMS:true, //optional default false
useLatLngOrder: true, //ordering of labels, default false-> lng-lat
});
// Add an optional legend
var legend = L.control({ position: "bottomleft" });
// Add the dialog box for messages
var dialogue = L.control.dialog({initOpen:false, size:[600,400], anchor:[50,150]}).addTo(map);
dialogue.freeze();
var doDialog = function(d) {
//console.log("DIALOGUE",d);
dialogue.setContent(d);
dialogue.open();
}
var helpText = '<h3>Node-RED - Map all the things</h3><br/>';
helpText += '<p><i class="fa fa-search fa-lg fa-fw"></i> <b>Search</b> - You may enter a name, or partial name, or icon name of an object to search for.';
helpText += 'The map will then jump to centre on each of the results in turn. If nothing is found locally it will try to';
helpText += 'search for a place name if connected to a network.</p>';
helpText += '<p><i class="fa fa-spinner fa-lg fa-fw"></i> <b>Set Max Age</b> - You can set the time after which points';
helpText += 'that haven\'t been updated get removed.</p>';
helpText += '<p><i class="fa fa-search-plus fa-lg fa-fw"></i> <b>Cluster at zoom</b> - lower numbers mean less clustering. 0 means disable totally.</p>';
helpText += '<p><i class="fa fa-arrows fa-lg fa-fw"></i> <b>Auto Pan</b> - When selected, the map will';
helpText += 'automatically move to centre on each data point as they arrive.</p>';
helpText += '<p><i class="fa fa-lock fa-lg fa-fw"></i> <b>Lock Map</b> - When selected will save the';
helpText += 'currently displayed area and basemap.';
helpText += 'Reloading the map in the current browser will return to the same view.';
helpText += 'This can be used to set your initial start position.';
helpText += 'While active it also restricts the "auto pan" and "search" to within that area.</p>';
helpText += '<p><i class="fa fa-globe fa-lg fa-fw"></i> <b>Heatmap all layers</b> - When selected';
helpText += 'all layers whether hidden or not will contribute to the heatmap.';
helpText += 'The default is that only visible layers add to the heatmap.</p>';
// Delete a marker (and notify websocket)
var delMarker = function(dname,note) {
if (note) { map.closePopup(); }
if (typeof polygons[dname] != "undefined") {
layers[polygons[dname].lay].removeLayer(polygons[dname]);
delete polygons[dname];
}
if (typeof polygons[dname+"_"] != "undefined") {
layers[polygons[dname+"_"].lay].removeLayer(polygons[dname+"_"]);
delete polygons[dname+"_"];
}
if (typeof markers[dname] != "undefined") {
layers[markers[dname].lay].removeLayer(markers[dname]);
map.removeLayer(markers[dname]);
delete markers[dname];
}
delete allData[dname];
if (note) { ws.send(JSON.stringify({action:"delete", name:dname, deleted:true})); }
}
var editPoly = function(pname) {
map.closePopup();
editFeatureGroup = L.featureGroup();
editToolbar = new L.EditToolbar({ featureGroup:editFeatureGroup });
editHandler = editToolbar.getModeHandlers()[0].handler;
editHandler._map = map;
polygons[pname].on("dblclick", function(e) {
editHandler.disable();
editFeatureGroup.removeLayer(polygons[pname]);
polygons[pname].off("dblclick");
L.DomEvent.stopPropagation(e);
var la, lo;
if (e.target.hasOwnProperty("_latlng")) {
la = e.target._latlng.lat;
lo = e.target._latlng.lng;
}
var m = {action:"draw", name:pname, layer:polygons[pname].lay, options:e.target.options, radius:e.target._mRadius, lat:la, lon:lo};
if (e.target.hasOwnProperty("_latlngs")) {
if (e.target.options.fill === false) { m.line = e.target._latlngs; }
else { m.area = e.target._latlngs[0]; }
}
ws.send(JSON.stringify(m));
})
editFeatureGroup.addLayer(polygons[pname]);
editHandler.enable();
}
var rangerings = function(latlng, options) {
options = L.extend({
ranges: [250,500,750,1000],
pan: 0,
fov: 60,
color: '#910000'
}, options);
var rings = L.featureGroup();
if (typeof options.ranges === "number") { options.ranges = [ options.ranges ]; }
for (var i = 0; i < options.ranges.length; i++) {
L.semiCircle(latlng, {
radius: options.ranges[i],
fill: false,
color: options.color,
weight: 1
}).setDirection(options.pan, options.fov).addTo(rings);
}
return rings;
}
// the MAIN add something to map function
function setMarker(data) {
var rightmenu = function(m) {
// customise right click context menu
var rightcontext = "";
if (polygons[data.name] == undefined) {
rightcontext = "<button id='delbutton' onclick='delMarker(\""+data.name+"\",true);'>Delete</button>";
}
else if (data.editable) {
rightcontext = "<button onclick='editPoly(\""+data.name+"\",true);'>Edit</button><button onclick='delMarker(\""+data.name+"\",true);'>Delete</button>";
}
if ((data.contextmenu !== undefined) && (typeof data.contextmenu === "string")) {
rightcontext = data.contextmenu.replace(/\$name/g,data.name);
delete data.contextmenu;
}
if (rightcontext.length > 0) {
var rightmenuMarker = L.popup({offset:[0,-12]}).setContent("<b>"+data.name+"</b><br/>"+rightcontext);
if (hiderightclick !== true) {
m.on('contextmenu', function(e) {
L.DomEvent.stopPropagation(e);
rightmenuMarker.setLatLng(e.latlng);
map.openPopup(rightmenuMarker);
});
}
}
else {
if (hiderightclick !== true) {
m.on('contextmenu', function(e) {
L.DomEvent.stopPropagation(e);
});
}
}
return m;
}
//console.log("DATA" typeof data, data);
if (data.deleted) { // remove markers we are told to
delMarker(data.name);
return;
}
var ll;
var lli = null;
var opt = {};
opt.color = data.color || data.lineColor || "#910000";
opt.fillColor = data.fillColor || "#910000";
opt.stroke = (data.hasOwnProperty("stroke")) ? data.stroke : true;
opt.weight = data.weight || 2;
opt.opacity = data.opacity || 1;
opt.fillOpacity = data.fillOpacity || 0.2;
opt.clickable = (data.hasOwnProperty("clickable")) ? data.clickable : false;
opt.fill = (data.hasOwnProperty("fill")) ? data.fill : true;
if (data.hasOwnProperty("dashArray")) { opt.dashArray = data.dashArray; }
// Replace building
if (data.hasOwnProperty("building")) {
if ((data.building === "") && layers.hasOwnProperty("buildings")) {
map.removeLayer(layers["buildings"]);
layercontrol._update();
layers["buildings"] = overlays["buildings"].set("");
return;
}
//layers["buildings"] = new OSMBuildings(map).set(data.building);
layers["buildings"] = overlays["buildings"].set(data.building);
map.addLayer(layers["buildings"]);
return;
}
var lay = data.layer || "unknown";
if (!data.hasOwnProperty("action") || data.action.indexOf("layer") === -1) {
if (typeof layers[lay] == "undefined") { // add layer if if doesn't exist
if (clusterAt > 0) {
layers[lay] = new L.MarkerClusterGroup({
maxClusterRadius:50,
spiderfyDistanceMultiplier:1.8,
disableClusteringAtZoom:clusterAt
//zoomToBoundsOnClick:false
});
}
else {
layers[lay] = new L.LayerGroup();
}
overlays[lay] = layers[lay];
if (showLayerMenu !== false) {
layercontrol.addOverlay(layers[lay],lay);
}
map.addLayer(overlays[lay]);
//console.log("ADDED LAYER",lay,layers);
}
if (!allData.hasOwnProperty(data.name)) { allData[data.name] = {}; }
delete data.action;
Object.keys(data).forEach(function(key) {
if (data[key] == null) { delete allData[data.name][key]; }
else { allData[data.name][key] = data[key]; }
});
data = Object.assign({},allData[data.name]);
}
delete data.action;
if (typeof markers[data.name] != "undefined") {
if (markers[data.name].lay !== lay) {
delMarker(data.name);
}
else {
try {layers[lay].removeLayer(markers[data.name]); }
catch(e) { console.log("OOPS"); }
}
}
if (typeof polygons[data.name] != "undefined") { layers[lay].removeLayer(polygons[data.name]); }
if (data.hasOwnProperty("line") && Array.isArray(data.line)) {
delete opt.fill;
if (!data.hasOwnProperty("weight")) { opt.weight = 3; } //Standard settings different for lines
if (!data.hasOwnProperty("opacity")) { opt.opacity = 0.8; }
var polyln = L.polyline(data.line, opt);
polygons[data.name] = polyln;
}
else if (data.hasOwnProperty("area") && Array.isArray(data.area)) {
var polyarea;
if (data.area.length === 2) { polyarea = L.rectangle(data.area, opt); }
else { polyarea = L.polygon(data.area, opt); }
polygons[data.name] = polyarea;
}
else if (data.hasOwnProperty("sdlat") && data.hasOwnProperty("sdlon")) {
if (!data.hasOwnProperty("iconColor")) { opt.color = "blue"; } //different standard Color Settings
if (!data.hasOwnProperty("fillColor")) { opt.fillColor = "blue"; }
var ellipse = L.ellipse(new L.LatLng((data.lat*1), (data.lon*1)), [200000*data.sdlon*Math.cos(data.lat*Math.PI/180), 200000*data.sdlat], 0, opt);
polygons[data.name] = ellipse;
}
else if (data.hasOwnProperty("radius")) {
if (data.hasOwnProperty("lat") && data.hasOwnProperty("lon")) {
var polycirc;
if (Array.isArray(data.radius)) {
polycirc = L.ellipse(new L.LatLng((data.lat*1), (data.lon*1)), [data.radius[0]*Math.cos(data.lat*Math.PI/180), data.radius[1]], data.tilt || 0, opt);
}
else {
polycirc = L.circle(new L.LatLng((data.lat*1), (data.lon*1)), data.radius*1, opt);
}
polygons[data.name] = polycirc;
delete (data.lat);
delete (data.lon);
}
}
else if (data.hasOwnProperty("arc")) {
if (data.hasOwnProperty("lat") && data.hasOwnProperty("lon")) {
polygons[data.name] = rangerings(new L.LatLng((data.lat*1), (data.lon*1)), data.arc);
}
}
else if (data.hasOwnProperty("geojson")) {
doGeojson(data.geojson,(data.layer || "geojson"),opt);
}
if (polygons[data.name] !== undefined) {
polygons[data.name].lay = lay;
if (opt.clickable === true) {
var words = "<b>"+data.name+"</b>";
if (data.popup) { words = words + "<br/>" + data.popup; }
polygons[data.name].bindPopup(words, {autoClose:false, closeButton:true, closeOnClick:false, minWidth:200});
}
//polygons[data.name] = rightmenu(polygons[data.name]); // DCJ Investigate
layers[lay].addLayer(polygons[data.name]);
}
if (typeof data.coordinates == "object") { ll = new L.LatLng(data.coordinates[1],data.coordinates[0]); }
else if (data.hasOwnProperty("position") && data.position.hasOwnProperty("lat") && data.position.hasOwnProperty("lon")) {
data.lat = data.position.lat*1;
data.lon = data.position.lon*1;
data.alt = data.position.alt;
if (parseFloat(data.position.alt) == data.position.alt) { data.alt = data.position.alt + " m"; }
delete data.position;
ll = new L.LatLng((data.lat*1), (data.lon*1));
}
else if (data.hasOwnProperty("lat") && data.hasOwnProperty("lon")) { ll = new L.LatLng((data.lat*1), (data.lon*1)); }
else if (data.hasOwnProperty("latitude") && data.hasOwnProperty("longitude")) { ll = new L.LatLng((data.latitude*1), (data.longitude*1)); }
else {
// console.log("No location:",data);
return;
}
// Adding new L.LatLng object (lli) when optional intensity value is defined. Only for use in heatmap layer
if (typeof data.coordinates == "object") { lli = new L.LatLng(data.coordinates[2],data.coordinates[1],data.coordinates[0]); }
else if (data.hasOwnProperty("lat") && data.hasOwnProperty("lon") && data.hasOwnProperty("intensity")) { lli = new L.LatLng((data.lat*1), (data.lon*1), (data.intensity*1)); }
else if (data.hasOwnProperty("latitude") && data.hasOwnProperty("longitude") && data.hasOwnProperty("intensity")) { lli = new L.LatLng((data.latitude*1), (data.longitude*1), (data.intensity*1)); }
else { lli = ll }
// Create the icons... handle plane, car, ship, wind, earthquake as specials
var marker, myMarker;
var icon, q;
var words = "";
var labelOffset = [12,0];
var drag = false;
if (data.draggable === true) { drag = true; }
if (data.hasOwnProperty("icon")) {
if (data.icon === "ship") {
marker = L.boatMarker(ll, {
title: data.name,
color: (data.iconColor || "blue")
});
marker.setHeading(parseFloat(data.hdg || data.bearing || "0"));
q = 'https://www.bing.com/images/search?q='+data.icon+'%20%2B"'+encodeURIComponent(data.name)+'"';
words += '<a href=\''+q+'\' target="_thingpic">Pictures</a><br>';
}
else if (data.icon === "plane") {
data.iconColor = data.iconColor || "black";
if (data.hasOwnProperty("squawk")) {
if (data.squawk == 7500 || data.squawk == 7600 || data.squawk == 7700) {
data.iconColor = "red";
}
}
icon = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="310px" height="310px" viewBox="0 0 310 310">';
icon += '<path d="M134.875,19.74c0.04-22.771,34.363-22.771,34.34,0.642v95.563L303,196.354v35.306l-133.144-43.821v71.424l30.813,24.072v27.923l-47.501-14.764l-47.501,14.764v-27.923l30.491-24.072v-71.424L3,231.66v-35.306l131.875-80.409V19.74z" fill="'+data.iconColor+'"/></svg>';
var svgplane = "data:image/svg+xml;base64," + btoa(icon);
var dir = parseFloat(data.hdg || data.bearing || "0");
myMarker = L.divIcon({
className:"planeicon",
iconAnchor: [16, 16],
html:'<img src="'+svgplane+'" style="width:32px; height:32px; -webkit-transform:rotate('+dir+'deg); -moz-transform:rotate('+dir+'deg);"/>'
});
marker = L.marker(ll, {title:data.name, icon:myMarker, draggable:drag});
//q = 'https://www.bing.com/images/search?q='+data.icon+'%20'+encodeURIComponent(data.name);
//words += '<a href=\''+q+'\' target="_thingpic">Pictures</a><br>';
}
else if (data.icon === "helicopter") {
data.iconColor = data.iconColor || "black";
if (data.hasOwnProperty("squawk")) {
if (data.squawk == 7500 || data.squawk == 7600 || data.squawk == 7700) {
data.iconColor = "red";
}
}
icon = '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="314" height="314" viewBox="0 0 314.5 314.5">';
icon += '<path d="M268.8 3c-3.1-3.1-8.3-2.9-11.7 0.5L204.9 55.7C198.5 23.3 180.8 0 159.9 0c-21.9 0-40.3 25.5-45.7 60.2L57.4 3.5c-3.4-3.4-8.6-3.6-11.7-0.5 -3.1 3.1-2.9 8.4 0.5 11.7l66.3 66.3c0 0.2 0 0.4 0 0.6 0 20.9 4.6 39.9 12.1 54.4l-78.4 78.4c-3.4 3.4-3.6 8.6-0.5 11.7 3.1 3.1 8.3 2.9 11.7-0.5l76.1-76.1c3.2 3.7 6.7 6.7 10.4 8.9v105.8l-47.7 32.2v18l50.2-22.3h26.9l50.2 22.3v-18L175.8 264.2v-105.8c2.7-1.7 5.4-3.8 7.8-6.2l73.4 73.4c3.4 3.4 8.6 3.6 11.7 0.5 3.1-3.1 2.9-8.3-0.5-11.7l-74.9-74.9c8.6-14.8 14-35.2 14-57.8 0-1.9-0.1-3.8-0.2-5.8l61.2-61.2C271.7 11.3 271.9 6.1 268.8 3z" fill="'+data.iconColor+'"/></svg>';
var svgheli = "data:image/svg+xml;base64," + btoa(icon);
var dir =