update 1.0.2 - fix 0/0 pos, enhanced prediction

upa v1.0.2
Hansi, dl9rdz 2021-01-29 13:20:12 +01:00
rodzic 9a49b48f4c
commit 1bfc9f8369
9 zmienionych plików z 913 dodań i 51 usunięć

Wyświetl plik

@ -0,0 +1,12 @@
www/js/leaflet.contextmenu.js
www/css/leaflet.contextmenu.css
The MIT License (MIT)
Copyright (c) 2017 adam.ratcliffe@gmail.com
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Wyświetl plik

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="de.dl9rdz" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0"
<widget id="de.dl9rdz" version="1.0.2" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0"
xmlns:android="schemas.android.com/apk/res/android">
<name>rdzSondyGO</name>
<description>

Wyświetl plik

@ -1,7 +1,7 @@
{
"name": "de.dl9rdz",
"displayName": "rdzwx-go",
"version": "0.0.1",
"version": "1.0.1",
"description": "A sample Apache Cordova application that uses rdzwx-plugin.",
"main": "index.js",
"scripts": {
@ -22,11 +22,12 @@
"cordova": {
"plugins": {
"cordova-plugin-whitelist": {},
"de-dl9rdz-rdzwx": {},
"cordova-plugin-inappbrowser": {}
"cordova-plugin-inappbrowser": {},
"de-dl9rdz-rdzwx": {}
},
"platforms": [
"android"
"android",
"browser"
]
}
}

Wyświetl plik

@ -49,6 +49,25 @@ html, body {
width: 100cv;
}
#toolbar {
background: rgba(255, 255, 255, 1);
opacity: .6;
width: 100vw;
height: 600px;
height: 100vh;
position: absolute;
left: -100vw;
z-index: 2000;
transition: .6s left;
padding: 20px;
box-sizing: border-box;
}
#toolbar.open {
left: 0;
opacity: 0.9;
}
.sondeTooltip {
color: #333;
font-size: 11px;
@ -88,6 +107,34 @@ html, body {
float: right;
}
.tawhiridiv {
background-color: #ecfedf;
padding: 1px 8px;
margin: 0px 0px !important;
}
.tawhiridiv td {
padding: 0px;
position: relative;
}
.tawhiridiv table {
border-spacing: 0;
}
.tawhiricontent {
clear: both;
font-size: 11pt;
}
.tawhiricontent input {
font-size: 11pt;
width: 5em;
}
.tawhiricontent input[type=checkbox] {
-webkit-transform: scale(1.5) !important;
}
.tawhirismall {
margin: 0pt !important;
font-size: 10pt;
}
.infocontent p {
display: inline-block;
}
@ -144,6 +191,29 @@ html, body {
}
}
.ballon-menu .leaflet-popup-tip {
background: rgba(0,0,0,0) !important;
box-shadow: none !important;
width: 0px;
height: 0px;
}
.ballon-drop {
background:#f8f8f8;
border: 1px solid #ccc;
border-radius:3px;
}
.ballon-drop a {
color:#333;
display: block;
padding: 5px 10px;
text-decoration:none;
}
.ballon-drop a:hover {
background:#333;
color:#fff;
}
.pop-header {
display: flex;
align-items:center;

Wyświetl plik

@ -0,0 +1,54 @@
.leaflet-contextmenu {
display: none;
box-shadow: 0 1px 7px rgba(0,0,0,0.4);
-webkit-border-radius: 4px;
border-radius: 4px;
padding: 4px 0;
background-color: #fff;
cursor: default;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.leaflet-contextmenu a.leaflet-contextmenu-item {
display: block;
color: #222;
font-size: 12px;
line-height: 20px;
text-decoration: none;
padding: 0 12px;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
cursor: default;
outline: none;
}
.leaflet-contextmenu a.leaflet-contextmenu-item-disabled {
opacity: 0.5;
}
.leaflet-contextmenu a.leaflet-contextmenu-item.over {
background-color: #f4f4f4;
border-top: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
}
.leaflet-contextmenu a.leaflet-contextmenu-item-disabled.over {
background-color: inherit;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
}
.leaflet-contextmenu-icon {
margin: 2px 8px 0 0;
width: 16px;
height: 16px;
float: left;
border: 0;
}
.leaflet-contextmenu-separator {
border-bottom: 1px solid #ccc;
margin: 5px 0;
}

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 776 B

Wyświetl plik

@ -36,8 +36,10 @@
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="css/index.css">
<link rel="stylesheet" href="css/leaflet.css">
<link rel="stylesheet" href="css/leaflet.contextmenu.css">
<script src="cordova.js"></script>
<script src="js/leaflet.js"></script>
<script src="js/leaflet.contextmenu.js"></script>
<link rel="stylesheet" href="css/easy-button.css">
<!--
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet-easybutton@2/src/easy-button.css">
@ -49,8 +51,17 @@
</head>
<body>
<div id="all">
<div id="toolbar">
<a id="toolbarclose" class="leaflet-popup-close-button" style="cursor: pointer; float: right;">x</a>
<h2>rdzSondyGO v1.0.1</h2>
<p>Copyright &copy; 2021 Hansi Reiser, dl9rdz</p>
<p>see <a href="https://github.com/dl9rdz/rdzwx-go">https://github.com/dl9rdz/rdzwx-go</a> for details</p>
<p>Apache License Version 2.0</p>
<hr>
<div id="toolbaritems">
</div>
</div>
<div id='map'></div>
<!-- <div id='info'></div> -->
</div>
<script src="js/index.js"></script>
</body>

Wyświetl plik

@ -25,12 +25,14 @@ document.addEventListener('deviceready', onDeviceReady, false);
var markers = {};
var ready = 0;
var map = null;
var lastObj = { obj: null, pred: null, land: null };
/////var lastObj = { obj: null, marker: null, /*no longer used: */pred: null, land: null };
var lastMarker = null;
var mypos = {lat: 48.56, lon: 13.43};
//var mypos = {lat: 48.1, lon: 13.1};
var myposMarker = null;
var ballonIcon, landIcon;
var ballonIcon, landIcon, burstIcon;
var infobox = null;
//var checkMark = "&#9989;";
@ -78,8 +80,7 @@ function onDeviceReady() {
});
var hybrid = new L.layerGroup([sat, Stamen_TonerHybrid]);
map = L.map('map', { layers: [osm] } ).setView([48,13],12);
map = L.map('map', { layers: [osm], contextmenu: true, zoomControl: false} ).setView([48,13],12);
var baseMaps = {
"Openstreetmap": osm,
"Landscape": tfland,
@ -99,13 +100,74 @@ function onDeviceReady() {
L.control.scale({metric: true, imperial: false, position: "bottomright"}).addTo(map);
// prediction
L.easyButton('<span class="target">&target;</span>', function(btn, map) {
getPrediction();
// main menu
L.easyButton('<span class="target">&equiv;</span>', function(btn, map) {
toolbar = L.DomUtil.get("toolbar");
L.DomUtil.addClass(toolbar, "open");
toolbarclose = L.DomUtil.get("toolbarclose");
L.DomEvent.on(toolbarclose, 'click', function(e) {L.DomUtil.removeClass(toolbar, "open")});
}).addTo(map);
new L.Control.Zoom({position: "topleft" }).addTo(map);
// prediction
L.easyButton('<span id="targetbtn" class="target">&target;</span>', function(btn, map) {
getPrediction();
}).addTo(map);
t = L.DomUtil.get("targetbtn");
if(t) { L.DomEvent.on(t, 'contextmenu', function(e) { tawhiriCtl.toggle(); } ); }
map.locate({setView: true, maxZoom: 16});
var TawhiriCtl = L.Control.extend({
options: { position: 'bottomcenter' },
onAdd: function(map) {
var tawhiriContainer = L.DomUtil.create('div', 'leaflet-control-layers leaflet-control');
var tawhiriBody = L.DomUtil.create('div', 'leaflet-popup-content-wrapper');
tawhiriContainer.appendChild(tawhiriBody);
var tawhiriContent = L.DomUtil.create('div', 'leaflet-popup-content tawhiridiv');
tawhiriBody.appendChild(tawhiriContent);
var infoCloseButton = L.DomUtil.create('a', 'leaflet-popup-close-button');
tawhiriContainer.appendChild(infoCloseButton);
infoCloseButton.innerHTML = 'x';
infoCloseButton.setAttribute('style', 'cursor: pointer');
var infoContent = L.DomUtil.create('div', 'tawhiricontent');
infoContent.innerHTML = '<h3>Tawhiri prediction parameter</h3><form><table>' +
'<tr><td>Ascent rate:</td><td><input type="number" size="5" value="5.0" step="any" id="tawhiri-ascent"></input></td><td>m/s</td></tr>' +
'<tr><td>Burst alt:</td><td><input type="number" size="5" value="35000" step="any" id="tawhiri-burst"></input></td><td>m</td></tr>' +
'<tr><td>Sea-level descent rate:</td><td><input type="number" size="5" value="5.0" step="any" id="tawhiri-descent"></input></td><td>m/s</td></tr>' +
'<tr><td>Use current v<sub>v</sub>:</td><td><input type="checkbox" checked="yes" id="tawhiri-current"></input></td><td></td><tr>' +
'<tr><td colspan="3"><p class="tawhirismall">If checked: On ascent: ascent rate := v<sub>v</sub><br>' +
'On descent: descent rate := estimate_sea_level(v<sub>v</sub>)</p></td></tr>';
'</table></form>'+
tawhiriContent.appendChild(infoContent);
this._tawhiriBody = tawhiriBody;
this._infoCloseButton = infoCloseButton;
this._showContent();
L.DomEvent.disableClickPropagation(tawhiriContainer);
L.DomEvent.on(infoCloseButton, 'click', L.DomEvent.stop);
L.DomEvent.on(infoCloseButton, 'click', this._hideContent, this);
return tawhiriContainer;
},
toggle: function() {
if(this._contentShown==false) { this._showContent(); } else { this._hideContent(); }
},
_hideContent: function(ev) {
this._tawhiriBody.style.display = 'none';
this._infoCloseButton.style.display = 'none';
this._contentShown = false;
},
_showContent: function(ev) {
this._tawhiriBody.style.display = '';
this._infoCloseButton.style.display = '';
this._contentShown = true;
},
})
tawhiriCtl = new TawhiriCtl();
tawhiriCtl.addTo(map);
var Infobox = L.Control.extend({
options: { position: 'bottomcenter' },
@ -154,12 +216,10 @@ function onDeviceReady() {
if(!this._infoContentContainer) return;
if(obj.type == null) obj.type = "RS41"; // TODO fix in plugin
distance = "";
if(obj.validPos) {
distance = L.latLng(obj).distanceTo(L.latLng(mypos))
if(distance>9999) { distance = distance.toFixed(0); }
else { distance = distance.toFixed(1); }
distance = "d=" + distance + "m";
}
distance = L.latLng(obj).distanceTo(L.latLng(mypos))
if(distance>9999) { distance = distance.toFixed(0); }
else { distance = distance.toFixed(1); }
distance = "d=" + distance + "m";
sym = "<span class=\"lifenessinfo\">&#x2B24; </span>";
l1 = "<table class=\"infotable\"><tr><td class=\"infotd\">" + sym + obj.type + "</td><td class=\"infotdr\">" + obj.ser + "</td></tr></table>";
l2 = "<table class=\"infotable\"><tr><td class=\"infotd\">" + (1*obj.freq).toFixed(3) + " MHz </td><td class=\"infotdr\" style=\”font-size:0.9em;\">" + (0.001*obj.afc).toFixed(2) + " kHz</td></tr></table>";
@ -195,11 +255,11 @@ function onDeviceReady() {
// fit map to enclosing rectangle of (last pos, own pos)
L.easyButton('<span class="fitbutton">&#9635;</span>', function(btn, map) {
// last item
if(lastObj.obj == null) return;
if(lastMarker == null) return;
// self position
if(mypos == null) return;
var items = [ [lastObj.obj.lat, lastObj.obj.lon], [mypos.lat, mypos.lon] ];
if(lastObj.land) { items.push( lastObj.land.getLatLng() ); }
var items = [ [lastMarker.obj.lat, lastMarker.obj.lon], [mypos.lat, mypos.lon] ];
if(lastMarker.land) { items.push( lastMarker.land.getLatLng() ); }
b = L.latLngBounds(items);
map.fitBounds(b);
}).addTo(map);
@ -234,12 +294,18 @@ function onDeviceReady() {
iconAnchor: [12,12],
popupAnchor: [0,0]
});
burstIcon = L.icon({
iconUrl: "img/pop-marker.png",
iconSize: [16,16],
iconAnchor: [8,8],
popupAnchor: [0,0]
});
ready = 1;
RdzWx.start("testarg", callBack);
setInterval(periodicStatusCheck, 1000);
// just for testing
update( {res: 0, validId: 1, validPos: 1, id: "A1234567", lat: 48, lon: 13, alt: 10000, vs: 10, hs: 30, rssi: -90, rxStat: "||||||||||||....", type: "RS41", freq: "400.000", afc: "+1.2", ser: "A1234567"} );
update( {res: 0, validId: 1, validPos: 127, id: "A1234567", lat: 48, lon: 13, alt: 10000, vs: 10, hs: 30, rssi: -90, rxStat: "||||||||||||....", type: "RS41", freq: "400.000", afc: "+1.2", ser: "A1234567"} );
updateMypos(mypos);
}
@ -277,33 +343,50 @@ function calc_drag(drag,alt){
return drag;
}
function getPrediction() {
function removePrediction(marker) {
if(marker.pred) { marker.pred.remove(map); }
if(marker.land) { marker.land.remove(map); }
if(marker.burst) { marker.burst.remove(map); }
}
function getPrediction(refobj) {
TAWHIRI = 'http://predict.cusf.co.uk/api/v1';
if(lastObj.obj == null) {
if(refobj == null) { refobj = lastMarker; }
if(refobj == null) {
alert("no object available");
return;
}
// lookup parameters from form
var burst = document.getElementById("tawhiri-burst").value;
if(burst) burst= parseInt(burst); else burst=35000;
if(refobj.obj.alt > burst) burst = refobj.obj.alt;
var asc = document.getElementById("tawhiri-ascent").value;
if(asc) asc=parseFloat(asc); else asc=5.0;
var desc = document.getElementById("tawhiri-descent").value;
if(desc) desc=parseFloat(desc); else desc=5.0;
var usecurrent = document.getElementById("tawhiri-current").checked;
var tParams = {
"launch_latitude": lastObj.obj.lat,
"launch_longitude": lastObj.obj.lon,
"launch_altitude": lastObj.obj.alt,
"launch_latitude": refobj.obj.lat,
"launch_longitude": refobj.obj.lon,
"launch_altitude": refobj.obj.alt,
"launch_datetime": new Date().toISOString().split('.')[0] + 'Z',
"ascent_rate": 5,
"descent_rate": 5,
"burst_altitude": lastObj.obj.alt+2,
"ascent_rate": asc,
"descent_rate": desc,
"burst_altitude": refobj.obj.alt+2,
"profile": "standard_profile",
}
var vs = lastObj.obj.vs;
if( markers[lastObj.obj.id] && markers[lastObj.obj.id].vsavg ) {
vs = markers[lastObj.obj.id].vsavg;
if(vs*lastObj.obj.vs < 0) vs=lastObj.obj.vs;
var vs = refobj.obj.vs;
if( refobj.vsavg ) {
vs = refobj.vsavg;
if(vs*refobj.obj.vs < 0) vs=refobj.obj.vs;
}
if(vs > 0) {
// still climbing up
tParams["ascent_rate"] = vs;
tParams["burst_altitude"] = 35000;
tParams["ascent_rate"] = usecurrent ? vs : asc;
tParams["burst_altitude"] = burst;
} else {
tParams["descent_rate"] = calc_drag( -vs, lastObj.obj.alt );
tParams["descent_rate"] = usecurrent ? calc_drag( -vs, refobj.obj.alt ) : desc;
}
const xhr = new XMLHttpRequest();
const url = TAWHIRI + formatParams(tParams);
@ -322,11 +405,17 @@ function getPrediction() {
//alert("path: "+JSON.stringify(traj));
poly = L.polyline(latlons, { opacity: 0.5, color: '#EE0000', dashArray: '8, 6'} );
poly.addTo(map);
if( lastObj.pred ) { lastObj.pred.remove(map); }
lastObj.pred = poly;
if( lastObj.land ) { lastObj.land.remove(map); }
lastObj.land = new L.marker(latlons.slice(-1)[0], {icon: landingIcon});
lastObj.land.addTo(map);
if( refobj.pred ) { refobj.pred.remove(map); }
refobj.pred = poly;
if( refobj.land ) { refobj.land.remove(map); }
refobj.land = new L.marker(latlons.slice(-1)[0], {icon: landingIcon});
refobj.land.addTo(map);
if( refobj.burst ) { refobj.burst.remove(map); }
if( vs>0 ) { // still climbing, so add burst mark
var b = traj0.slice(-1)[0];
refobj.burst = new L.marker( [b.latitude, b.longitude], {icon: burstIcon});
refobj.burst.addTo(map);
}
var lastpt = traj1.splice(-1)[0];
lastpt.datetime = new Date(lastpt.datetime).toISOString().split(".")[0] + "Z";
var popup = '<div class="pop-header"><img src="img/landing.png"><h4> Landing Point </h4></div>' +
@ -337,7 +426,7 @@ function getPrediction() {
'</br>Burst: ' + tParams["burst_altitude"] + ' m'+
'</br>Desc. Rate: ' + tParams["descent_rate"].toFixed(2) + ' m/s</p>' +
'';
lastObj.land.bindPopup(popup);
refobj.land.bindPopup(popup);
}
}
xhr.open('GET', url, true);
@ -403,6 +492,10 @@ function update(obj) {
}
// position update
if( ((obj.validPos&0x03) != 0x03) || ((obj.validPos&0x80)!=0) ) { // latitude and longitude are invalid
console.log("invalid position, ignoring");
return;
}
console.log("Pos update: "+JSON.stringify(obj));
infobox.setContent(obj);
infobox.setStatus(obj.res);
@ -413,7 +506,6 @@ function update(obj) {
console.log("update with no valid pos");
return;
}
lastObj.obj = obj;
console.log("Good update!");
var pos = new L.LatLng(obj.lat, obj.lon);
var marker;
@ -426,7 +518,29 @@ function update(obj) {
marker.vsavg = 0.9 * marker.vsavg + 0.1 * obj.vs;
} else {
console.log("creating new marker");
marker = new L.marker(pos, {icon: ballonIcon});
marker = new L.marker(pos, {icon: ballonIcon,
contextmenu: true,
contextmenuItems: [{
/*
text: "Show info",
callback: function(e) { alert("not available yet"); }
}, {
*/
text: "Make prediction",
callback: function(e) { getPrediction(marker); }
}, {
text: "Configure prediction",
callback: function(e) { tawhiriCtl.toggle(); }
}, {
text: "Remove prediction",
callback: function(e) { removePrediction(marker); }
}, {
separator: true
}, {
text: "Delete item",
callback: function(e) { removePrediction(marker); map.removeLayer(marker); if(marker==lastMarker) lastMarker=null; }
}]
});
poly = L.polyline(pos, { opacity: 0.5, color: '#3388ff'} );
marker.path = poly;
//marker.on('clock', function() { showMarkerInfoWindow( obj.id, pos) } );
@ -437,11 +551,19 @@ function update(obj) {
marker.bindTooltip(tooltip);
marker.tt = tooltip;
marker.vsavg = obj.vs;
}
var tt = '<div class="tooltip-container">' + obj.id + '<div class="text-speed tooltip-container">' + obj.alt + 'm '+ obj.vs +'m/s ' + (obj.hs*3.6).toFixed(1) + 'km/h </div></div>';
tooltip.setContent(tt);
}
lastMarker = marker;
lastMarker.obj = obj;
var tt = '<div class="tooltip-container">' + obj.id + '<div class="text-speed tooltip-container">' + obj.alt + 'm '+ obj.vs +'m/s ' + (obj.hs*3.6).toFixed(1) + 'km/h </div></div>';
tooltip.setContent(tt);
marker.setLatLng(pos);
marker.update(); // necessary?
marker.setLatLng(pos);
marker.update(); // necessary?
}
function createButton(label, container) {
var btn = L.DomUtil.create("button", "", container);
btn.setAttribute("type", "button");
btn.innerHTML = label;
return btn;
}

Wyświetl plik

@ -0,0 +1,592 @@
/*
Leaflet.contextmenu, a context menu for Leaflet.
(c) 2015, Adam Ratcliffe, GeoSmart Maps Limited
@preserve
*/
(function(factory) {
// Packaging/modules magic dance
var L;
if (typeof define === 'function' && define.amd) {
// AMD
define(['leaflet'], factory);
} else if (typeof module === 'object' && typeof module.exports === 'object') {
// Node/CommonJS
L = require('leaflet');
module.exports = factory(L);
} else {
// Browser globals
if (typeof window.L === 'undefined') {
throw new Error('Leaflet must be loaded first');
}
factory(window.L);
}
})(function(L) {
L.Map.mergeOptions({
contextmenuItems: []
});
L.Map.ContextMenu = L.Handler.extend({
_touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart',
statics: {
BASE_CLS: 'leaflet-contextmenu'
},
initialize: function (map) {
L.Handler.prototype.initialize.call(this, map);
this._items = [];
this._visible = false;
var container = this._container = L.DomUtil.create('div', L.Map.ContextMenu.BASE_CLS, map._container);
container.style.zIndex = 10000;
container.style.position = 'absolute';
if (map.options.contextmenuWidth) {
container.style.width = map.options.contextmenuWidth + 'px';
}
this._createItems();
L.DomEvent
.on(container, 'click', L.DomEvent.stop)
.on(container, 'mousedown', L.DomEvent.stop)
.on(container, 'dblclick', L.DomEvent.stop)
.on(container, 'contextmenu', L.DomEvent.stop);
},
addHooks: function () {
var container = this._map.getContainer();
L.DomEvent
.on(container, 'mouseleave', this._hide, this)
.on(document, 'keydown', this._onKeyDown, this);
if (L.Browser.touch) {
L.DomEvent.on(document, this._touchstart, this._hide, this);
}
this._map.on({
contextmenu: this._show,
mousedown: this._hide,
zoomstart: this._hide
}, this);
},
removeHooks: function () {
var container = this._map.getContainer();
L.DomEvent
.off(container, 'mouseleave', this._hide, this)
.off(document, 'keydown', this._onKeyDown, this);
if (L.Browser.touch) {
L.DomEvent.off(document, this._touchstart, this._hide, this);
}
this._map.off({
contextmenu: this._show,
mousedown: this._hide,
zoomstart: this._hide
}, this);
},
showAt: function (point, data) {
if (point instanceof L.LatLng) {
point = this._map.latLngToContainerPoint(point);
}
this._showAtPoint(point, data);
},
hide: function () {
this._hide();
},
addItem: function (options) {
return this.insertItem(options);
},
insertItem: function (options, index) {
index = index !== undefined ? index: this._items.length;
var item = this._createItem(this._container, options, index);
this._items.push(item);
this._sizeChanged = true;
this._map.fire('contextmenu.additem', {
contextmenu: this,
el: item.el,
index: index
});
return item.el;
},
removeItem: function (item) {
var container = this._container;
if (!isNaN(item)) {
item = container.children[item];
}
if (item) {
this._removeItem(L.Util.stamp(item));
this._sizeChanged = true;
this._map.fire('contextmenu.removeitem', {
contextmenu: this,
el: item
});
return item;
}
return null;
},
removeAllItems: function () {
var items = this._container.children,
item;
while (items.length) {
item = items[0];
this._removeItem(L.Util.stamp(item));
}
return items;
},
hideAllItems: function () {
var item, i, l;
for (i = 0, l = this._items.length; i < l; i++) {
item = this._items[i];
item.el.style.display = 'none';
}
},
showAllItems: function () {
var item, i, l;
for (i = 0, l = this._items.length; i < l; i++) {
item = this._items[i];
item.el.style.display = '';
}
},
setDisabled: function (item, disabled) {
var container = this._container,
itemCls = L.Map.ContextMenu.BASE_CLS + '-item';
if (!isNaN(item)) {
item = container.children[item];
}
if (item && L.DomUtil.hasClass(item, itemCls)) {
if (disabled) {
L.DomUtil.addClass(item, itemCls + '-disabled');
this._map.fire('contextmenu.disableitem', {
contextmenu: this,
el: item
});
} else {
L.DomUtil.removeClass(item, itemCls + '-disabled');
this._map.fire('contextmenu.enableitem', {
contextmenu: this,
el: item
});
}
}
},
isVisible: function () {
return this._visible;
},
_createItems: function () {
var itemOptions = this._map.options.contextmenuItems,
item,
i, l;
for (i = 0, l = itemOptions.length; i < l; i++) {
this._items.push(this._createItem(this._container, itemOptions[i]));
}
},
_createItem: function (container, options, index) {
if (options.separator || options === '-') {
return this._createSeparator(container, index);
}
var itemCls = L.Map.ContextMenu.BASE_CLS + '-item',
cls = options.disabled ? (itemCls + ' ' + itemCls + '-disabled') : itemCls,
el = this._insertElementAt('a', cls, container, index),
callback = this._createEventHandler(el, options.callback, options.context, options.hideOnSelect),
icon = this._getIcon(options),
iconCls = this._getIconCls(options),
html = '';
if (icon) {
html = '<img class="' + L.Map.ContextMenu.BASE_CLS + '-icon" src="' + icon + '"/>';
} else if (iconCls) {
html = '<span class="' + L.Map.ContextMenu.BASE_CLS + '-icon ' + iconCls + '"></span>';
}
el.innerHTML = html + options.text;
el.href = '#';
L.DomEvent
.on(el, 'mouseover', this._onItemMouseOver, this)
.on(el, 'mouseout', this._onItemMouseOut, this)
.on(el, 'mousedown', L.DomEvent.stopPropagation)
.on(el, 'click', callback);
if (L.Browser.touch) {
L.DomEvent.on(el, this._touchstart, L.DomEvent.stopPropagation);
}
// Devices without a mouse fire "mouseover" on tap, but never “mouseout"
if (!L.Browser.pointer) {
L.DomEvent.on(el, 'click', this._onItemMouseOut, this);
}
return {
id: L.Util.stamp(el),
el: el,
callback: callback
};
},
_removeItem: function (id) {
var item,
el,
i, l, callback;
for (i = 0, l = this._items.length; i < l; i++) {
item = this._items[i];
if (item.id === id) {
el = item.el;
callback = item.callback;
if (callback) {
L.DomEvent
.off(el, 'mouseover', this._onItemMouseOver, this)
.off(el, 'mouseover', this._onItemMouseOut, this)
.off(el, 'mousedown', L.DomEvent.stopPropagation)
.off(el, 'click', callback);
if (L.Browser.touch) {
L.DomEvent.off(el, this._touchstart, L.DomEvent.stopPropagation);
}
if (!L.Browser.pointer) {
L.DomEvent.on(el, 'click', this._onItemMouseOut, this);
}
}
this._container.removeChild(el);
this._items.splice(i, 1);
return item;
}
}
return null;
},
_createSeparator: function (container, index) {
var el = this._insertElementAt('div', L.Map.ContextMenu.BASE_CLS + '-separator', container, index);
return {
id: L.Util.stamp(el),
el: el
};
},
_createEventHandler: function (el, func, context, hideOnSelect) {
var me = this,
map = this._map,
disabledCls = L.Map.ContextMenu.BASE_CLS + '-item-disabled',
hideOnSelect = (hideOnSelect !== undefined) ? hideOnSelect : true;
return function (e) {
if (L.DomUtil.hasClass(el, disabledCls)) {
return;
}
var map = me._map,
containerPoint = me._showLocation.containerPoint,
layerPoint = map.containerPointToLayerPoint(containerPoint),
latlng = map.layerPointToLatLng(layerPoint),
relatedTarget = me._showLocation.relatedTarget,
data = {
containerPoint: containerPoint,
layerPoint: layerPoint,
latlng: latlng,
relatedTarget: relatedTarget
};
if (hideOnSelect) {
me._hide();
}
if (func) {
func.call(context || map, data);
}
me._map.fire('contextmenu.select', {
contextmenu: me,
el: el
});
};
},
_insertElementAt: function (tagName, className, container, index) {
var refEl,
el = document.createElement(tagName);
el.className = className;
if (index !== undefined) {
refEl = container.children[index];
}
if (refEl) {
container.insertBefore(el, refEl);
} else {
container.appendChild(el);
}
return el;
},
_show: function (e) {
this._showAtPoint(e.containerPoint, e);
},
_showAtPoint: function (pt, data) {
if (this._items.length) {
var map = this._map,
event = L.extend(data || {}, {contextmenu: this});
this._showLocation = {
containerPoint: pt
};
if (data && data.relatedTarget){
this._showLocation.relatedTarget = data.relatedTarget;
}
this._setPosition(pt);
if (!this._visible) {
this._container.style.display = 'block';
this._visible = true;
}
this._map.fire('contextmenu.show', event);
}
},
_hide: function () {
if (this._visible) {
this._visible = false;
this._container.style.display = 'none';
this._map.fire('contextmenu.hide', {contextmenu: this});
}
},
_getIcon: function (options) {
return L.Browser.retina && options.retinaIcon || options.icon;
},
_getIconCls: function (options) {
return L.Browser.retina && options.retinaIconCls || options.iconCls;
},
_setPosition: function (pt) {
var mapSize = this._map.getSize(),
container = this._container,
containerSize = this._getElementSize(container),
anchor;
if (this._map.options.contextmenuAnchor) {
anchor = L.point(this._map.options.contextmenuAnchor);
pt = pt.add(anchor);
}
container._leaflet_pos = pt;
if (pt.x + containerSize.x > mapSize.x) {
container.style.left = 'auto';
container.style.right = Math.min(Math.max(mapSize.x - pt.x, 0), mapSize.x - containerSize.x - 1) + 'px';
} else {
container.style.left = Math.max(pt.x, 0) + 'px';
container.style.right = 'auto';
}
if (pt.y + containerSize.y > mapSize.y) {
container.style.top = 'auto';
container.style.bottom = Math.min(Math.max(mapSize.y - pt.y, 0), mapSize.y - containerSize.y - 1) + 'px';
} else {
container.style.top = Math.max(pt.y, 0) + 'px';
container.style.bottom = 'auto';
}
},
_getElementSize: function (el) {
var size = this._size,
initialDisplay = el.style.display;
if (!size || this._sizeChanged) {
size = {};
el.style.left = '-999999px';
el.style.right = 'auto';
el.style.display = 'block';
size.x = el.offsetWidth;
size.y = el.offsetHeight;
el.style.left = 'auto';
el.style.display = initialDisplay;
this._sizeChanged = false;
}
return size;
},
_onKeyDown: function (e) {
var key = e.keyCode;
// If ESC pressed and context menu is visible hide it
if (key === 27) {
this._hide();
}
},
_onItemMouseOver: function (e) {
L.DomUtil.addClass(e.target || e.srcElement, 'over');
},
_onItemMouseOut: function (e) {
L.DomUtil.removeClass(e.target || e.srcElement, 'over');
}
});
L.Map.addInitHook('addHandler', 'contextmenu', L.Map.ContextMenu);
L.Mixin.ContextMenu = {
bindContextMenu: function (options) {
L.setOptions(this, options);
this._initContextMenu();
return this;
},
unbindContextMenu: function (){
this.off('contextmenu', this._showContextMenu, this);
return this;
},
addContextMenuItem: function (item) {
this.options.contextmenuItems.push(item);
},
removeContextMenuItemWithIndex: function (index) {
var items = [];
for (var i = 0; i < this.options.contextmenuItems.length; i++) {
if (this.options.contextmenuItems[i].index == index){
items.push(i);
}
}
var elem = items.pop();
while (elem !== undefined) {
this.options.contextmenuItems.splice(elem,1);
elem = items.pop();
}
},
replaceContextMenuItem: function (item) {
this.removeContextMenuItemWithIndex(item.index);
this.addContextMenuItem(item);
},
_initContextMenu: function () {
this._items = [];
this.on('contextmenu', this._showContextMenu, this);
},
_showContextMenu: function (e) {
var itemOptions,
data, pt, i, l;
if (this._map.contextmenu) {
data = L.extend({relatedTarget: this}, e);
pt = this._map.mouseEventToContainerPoint(e.originalEvent);
if (!this.options.contextmenuInheritItems) {
this._map.contextmenu.hideAllItems();
}
for (i = 0, l = this.options.contextmenuItems.length; i < l; i++) {
itemOptions = this.options.contextmenuItems[i];
this._items.push(this._map.contextmenu.insertItem(itemOptions, itemOptions.index));
}
this._map.once('contextmenu.hide', this._hideContextMenu, this);
this._map.contextmenu.showAt(pt, data);
}
},
_hideContextMenu: function () {
var i, l;
for (i = 0, l = this._items.length; i < l; i++) {
this._map.contextmenu.removeItem(this._items[i]);
}
this._items.length = 0;
if (!this.options.contextmenuInheritItems) {
this._map.contextmenu.showAllItems();
}
}
};
var classes = [L.Marker, L.Path],
defaultOptions = {
contextmenu: false,
contextmenuItems: [],
contextmenuInheritItems: true
},
cls, i, l;
for (i = 0, l = classes.length; i < l; i++) {
cls = classes[i];
// L.Class should probably provide an empty options hash, as it does not test
// for it here and add if needed
if (!cls.prototype.options) {
cls.prototype.options = defaultOptions;
} else {
cls.mergeOptions(defaultOptions);
}
cls.addInitHook(function () {
if (this.options.contextmenu) {
this._initContextMenu();
}
});
cls.include(L.Mixin.ContextMenu);
}
return L.Map.ContextMenu;
});