diff --git a/LICENSE-Leaflet.contextmenu b/LICENSE-Leaflet.contextmenu new file mode 100644 index 0000000..1ee8d53 --- /dev/null +++ b/LICENSE-Leaflet.contextmenu @@ -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. diff --git a/config.xml b/config.xml index 5ff2030..d2c1fe9 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - rdzSondyGO diff --git a/package.json b/package.json index f2c57e5..0a7ec12 100644 --- a/package.json +++ b/package.json @@ -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" ] } } diff --git a/www/css/index.css b/www/css/index.css index 7901f15..0fd5702 100644 --- a/www/css/index.css +++ b/www/css/index.css @@ -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; diff --git a/www/css/leaflet.contextmenu.css b/www/css/leaflet.contextmenu.css new file mode 100644 index 0000000..0b5e2de --- /dev/null +++ b/www/css/leaflet.contextmenu.css @@ -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; +} diff --git a/www/img/pop-marker.png b/www/img/pop-marker.png new file mode 100644 index 0000000..a111bfe Binary files /dev/null and b/www/img/pop-marker.png differ diff --git a/www/index.html b/www/index.html index 7525b70..16cf311 100644 --- a/www/index.html +++ b/www/index.html @@ -36,8 +36,10 @@ + + diff --git a/www/js/index.js b/www/js/index.js index 2594a91..e8fc0ef 100644 --- a/www/js/index.js +++ b/www/js/index.js @@ -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 = "✅"; @@ -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('', function(btn, map) { - getPrediction(); + // main menu + L.easyButton('', 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('', 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 = '

Tawhiri prediction parameter

' + + '' + + '' + + '' + + '' + + ''; + '
Ascent rate:m/s
Burst alt:m
Sea-level descent rate:m/s
Use current vv:

If checked: On ascent: ascent rate := vv
' + + 'On descent: descent rate := estimate_sea_level(vv)

'+ + 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 = ""; l1 = "
" + sym + obj.type + "" + obj.ser + "
"; l2 = "
" + (1*obj.freq).toFixed(3) + " MHz " + (0.001*obj.afc).toFixed(2) + " kHz
"; @@ -195,11 +255,11 @@ function onDeviceReady() { // fit map to enclosing rectangle of (last pos, own pos) L.easyButton('', 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 = '

Landing Point

' + @@ -337,7 +426,7 @@ function getPrediction() { '
Burst: ' + tParams["burst_altitude"] + ' m'+ '
Desc. Rate: ' + tParams["descent_rate"].toFixed(2) + ' m/s

' + ''; - 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 = '
' + obj.id + '
' + obj.alt + 'm '+ obj.vs +'m/s ' + (obj.hs*3.6).toFixed(1) + 'km/h
'; - tooltip.setContent(tt); + } + lastMarker = marker; + lastMarker.obj = obj; + var tt = '
' + obj.id + '
' + obj.alt + 'm '+ obj.vs +'m/s ' + (obj.hs*3.6).toFixed(1) + 'km/h
'; + 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; +} diff --git a/www/js/leaflet.contextmenu.js b/www/js/leaflet.contextmenu.js new file mode 100644 index 0000000..489da87 --- /dev/null +++ b/www/js/leaflet.contextmenu.js @@ -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 = ''; + } else if (iconCls) { + html = ''; + } + + 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; +});