commit fa8146e8c4ade8bf8369e57a66f15c1f828f0a99 Author: Raoul Snyman Date: Tue Apr 11 13:51:59 2023 -0700 Initial commit diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ed1decd --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023-present Raoul Snyman + +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/README.md b/README.md new file mode 100644 index 0000000..1348837 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# MapMakr + +[![PyPI - Version](https://img.shields.io/pypi/v/mapmakr.svg)](https://pypi.org/project/mapmakr) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/mapmakr.svg)](https://pypi.org/project/mapmakr) + +----- + +**Table of Contents** + +- [Installation](#installation) +- [License](#license) + +## Installation + +```console +pip install mapmakr +``` + +## License + +`mapmakr` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. diff --git a/mapmakr/__about__.py b/mapmakr/__about__.py new file mode 100644 index 0000000..0730df7 --- /dev/null +++ b/mapmakr/__about__.py @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: 2023-present Raoul Snyman +# +# SPDX-License-Identifier: MIT +__version__ = '0.0.1' diff --git a/mapmakr/__init__.py b/mapmakr/__init__.py new file mode 100644 index 0000000..5b501aa --- /dev/null +++ b/mapmakr/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2023-present Raoul Snyman +# +# SPDX-License-Identifier: MIT diff --git a/mapmakr/app.py b/mapmakr/app.py new file mode 100644 index 0000000..6fc2f5c --- /dev/null +++ b/mapmakr/app.py @@ -0,0 +1,15 @@ +from pathlib import Path + +from fastapi import FastAPI +from fastapi.responses import HTMLResponse +from fastapi.staticfiles import StaticFiles + +ROOT = Path(__file__).parent +app = FastAPI() +app.mount('/static', StaticFiles(directory='static'), name='static') + + +@app.get('/', response_class=HTMLResponse) +async def index(): + content = (ROOT / 'templates' / 'index.html').open().read() + return HTMLResponse(content=content, status_code=200) diff --git a/mapmakr/static/3rdparty/L.Terminator.js b/mapmakr/static/3rdparty/L.Terminator.js new file mode 100644 index 0000000..8e21458 --- /dev/null +++ b/mapmakr/static/3rdparty/L.Terminator.js @@ -0,0 +1,143 @@ +/* Terminator.js -- Overlay day/night region on a Leaflet map */ + +Date.prototype.getJulian = function() { + /* Calculate the present UTC Julian Date. Function is valid after + * the beginning of the UNIX epoch 1970-01-01 and ignores leap + * seconds. */ + return (this / 86400000) + 2440587.5; +} + +Date.prototype.getGMST = function() { + /* Calculate Greenwich Mean Sidereal Time according to + http://aa.usno.navy.mil/faq/docs/GAST.php */ + var julianDay = this.getJulian(); + var d = julianDay - 2451545.0; + // Low precision equation is good enough for our purposes. + return (18.697374558 + 24.06570982441908 * d) % 24; +} + +L.Terminator = L.Polygon.extend({ + options: { + color: '#00', + opacity: 0.5, + fillColor: '#00', + fillOpacity: 0.5, + resolution: 2 + }, + + initialize: function(options) { + this.version = '0.1.0'; + this._R2D = 180 / Math.PI; + this._D2R = Math.PI / 180; + L.Util.setOptions(this, options); + var latLng = this._compute(this.options.time || null) + this.setLatLngs(latLng); + }, + + setTime: function(date) { + this.options.time = date; + var latLng = this._compute(date || null) + this.setLatLngs(latLng); + }, + + _sunEclipticPosition: function(julianDay) { + /* Compute the position of the Sun in ecliptic coordinates at + julianDay. Following + http://en.wikipedia.org/wiki/Position_of_the_Sun */ + // Days since start of J2000.0 + var n = julianDay - 2451545.0; + // mean longitude of the Sun + var L = 280.460 + 0.9856474 * n; + L %= 360; + // mean anomaly of the Sun + var g = 357.528 + 0.9856003 * n; + g %= 360; + // ecliptic longitude of Sun + var lambda = L + 1.915 * Math.sin(g * this._D2R) + + 0.02 * Math.sin(2 * g * this._D2R); + // distance from Sun in AU + var R = 1.00014 - 0.01671 * Math.cos(g * this._D2R) - + 0.0014 * Math.cos(2 * g * this._D2R); + return {"lambda": lambda, "R": R}; + }, + + _eclipticObliquity: function(julianDay) { + // Following the short term expression in + // http://en.wikipedia.org/wiki/Axial_tilt#Obliquity_of_the_ecliptic_.28Earth.27s_axial_tilt.29 + var n = julianDay - 2451545.0; + // Julian centuries since J2000.0 + var T = n / 36525; + var epsilon = 23.43929111 - + T * (46.836769 / 3600 + - T * (0.0001831 / 3600 + + T * (0.00200340 / 3600 + - T * (0.576e-6 / 3600 + - T * 4.34e-8 / 3600)))); + return epsilon; + }, + + _sunEquatorialPosition: function(sunEclLng, eclObliq) { + /* Compute the Sun's equatorial position from its ecliptic + * position. Inputs are expected in degrees. Outputs are in + * degrees as well. */ + var alpha = Math.atan(Math.cos(eclObliq * this._D2R) + * Math.tan(sunEclLng * this._D2R)) * this._R2D; + var delta = Math.asin(Math.sin(eclObliq * this._D2R) + * Math.sin(sunEclLng * this._D2R)) * this._R2D; + + var lQuadrant = Math.floor(sunEclLng / 90) * 90; + var raQuadrant = Math.floor(alpha / 90) * 90; + alpha = alpha + (lQuadrant - raQuadrant); + + return {"alpha": alpha, "delta": delta}; + }, + + _hourAngle: function(lng, sunPos, gst) { + /* Compute the hour angle of the sun for a longitude on + * Earth. Return the hour angle in degrees. */ + var lst = gst + lng / 15; + return lst * 15 - sunPos.alpha; + }, + + _latitude: function(ha, sunPos) { + /* For a given hour angle and sun position, compute the + * latitude of the terminator in degrees. */ + var lat = Math.atan(-Math.cos(ha * this._D2R) / + Math.tan(sunPos.delta * this._D2R)) * this._R2D; + return lat; + }, + + _compute: function(time) { + if (time == null) + var today = new Date(); + else + var today = new Date(time); + var julianDay = today.getJulian(); + var gst = today.getGMST(); + var latLng = []; + var ha, lat; + + var sunEclPos = this._sunEclipticPosition(julianDay); + var eclObliq = this._eclipticObliquity(julianDay); + var sunEqPos = this._sunEquatorialPosition(sunEclPos.lambda, eclObliq); + for (var i = 0; i <= 720 * this.options.resolution; i++) { + lng = -360 + i / this.options.resolution; + ha = this._hourAngle(lng, sunEqPos, gst); + lat = this._latitude(ha, sunEqPos); + latLng[i+1] = [lat, lng]; + } + if (sunEqPos.delta < 0) { + latLng[0] = [90, -360]; + latLng[latLng.length] = [90, 360]; + } else { + latLng[0] = [-90, -360]; + latLng[latLng.length] = [-90, 360]; + } + return latLng; + } +}); + +L.terminator = function(options) { + return new L.Terminator(options); +}; + diff --git a/mapmakr/static/3rdparty/MarkerCluster.Default.css b/mapmakr/static/3rdparty/MarkerCluster.Default.css new file mode 100644 index 0000000..bbc8c9f --- /dev/null +++ b/mapmakr/static/3rdparty/MarkerCluster.Default.css @@ -0,0 +1,60 @@ +.marker-cluster-small { + background-color: rgba(181, 226, 140, 0.6); + } +.marker-cluster-small div { + background-color: rgba(110, 204, 57, 0.6); + } + +.marker-cluster-medium { + background-color: rgba(241, 211, 87, 0.6); + } +.marker-cluster-medium div { + background-color: rgba(240, 194, 12, 0.6); + } + +.marker-cluster-large { + background-color: rgba(253, 156, 115, 0.6); + } +.marker-cluster-large div { + background-color: rgba(241, 128, 23, 0.6); + } + + /* IE 6-8 fallback colors */ +.leaflet-oldie .marker-cluster-small { + background-color: rgb(181, 226, 140); + } +.leaflet-oldie .marker-cluster-small div { + background-color: rgb(110, 204, 57); + } + +.leaflet-oldie .marker-cluster-medium { + background-color: rgb(241, 211, 87); + } +.leaflet-oldie .marker-cluster-medium div { + background-color: rgb(240, 194, 12); + } + +.leaflet-oldie .marker-cluster-large { + background-color: rgb(253, 156, 115); + } +.leaflet-oldie .marker-cluster-large div { + background-color: rgb(241, 128, 23); +} + +.marker-cluster { + background-clip: padding-box; + border-radius: 20px; + } +.marker-cluster div { + width: 30px; + height: 30px; + margin-left: 5px; + margin-top: 5px; + + text-align: center; + border-radius: 15px; + font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif; + } +.marker-cluster span { + line-height: 30px; + } \ No newline at end of file diff --git a/mapmakr/static/3rdparty/MarkerCluster.css b/mapmakr/static/3rdparty/MarkerCluster.css new file mode 100644 index 0000000..c60d71b --- /dev/null +++ b/mapmakr/static/3rdparty/MarkerCluster.css @@ -0,0 +1,14 @@ +.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow { + -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in; + -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in; + -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in; + transition: transform 0.3s ease-out, opacity 0.3s ease-in; +} + +.leaflet-cluster-spider-leg { + /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */ + -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in; + -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in; + -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in; + transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in; +} diff --git a/mapmakr/static/3rdparty/leaflet-easybutton.css b/mapmakr/static/3rdparty/leaflet-easybutton.css new file mode 100644 index 0000000..670b220 --- /dev/null +++ b/mapmakr/static/3rdparty/leaflet-easybutton.css @@ -0,0 +1,56 @@ +.leaflet-bar button, +.leaflet-bar button:hover { + background-color: #fff; + border: none; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; +} + +.leaflet-bar button { + background-position: 50% 50%; + background-repeat: no-repeat; + overflow: hidden; + display: block; +} + +.leaflet-bar button:hover { + background-color: #f4f4f4; +} + +.leaflet-bar button:first-of-type { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.leaflet-bar button:last-of-type { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; +} + +.leaflet-bar.disabled, +.leaflet-bar button.disabled { + cursor: default; + pointer-events: none; + opacity: .4; +} + +.easy-button-button .button-state{ + display: block; + width: 100%; + heigth: 100%; + position: relative; +} + + +.leaflet-touch .leaflet-bar button { + width: 30px; + height: 30px; + line-height: 30px; +} diff --git a/mapmakr/static/3rdparty/leaflet-easybutton.js b/mapmakr/static/3rdparty/leaflet-easybutton.js new file mode 100644 index 0000000..6401f54 --- /dev/null +++ b/mapmakr/static/3rdparty/leaflet-easybutton.js @@ -0,0 +1,366 @@ +(function(){ + +// This is for grouping buttons into a bar +// takes an array of `L.easyButton`s and +// then the usual `.addTo(map)` +L.Control.EasyBar = L.Control.extend({ + + options: { + position: 'topleft', // part of leaflet's defaults + id: null, // an id to tag the Bar with + leafletClasses: true // use leaflet classes? + }, + + + initialize: function(buttons, options){ + + if(options){ + L.Util.setOptions( this, options ); + } + + this._buildContainer(); + this._buttons = []; + + for(var i = 0; i < buttons.length; i++){ + buttons[i]._bar = this; + buttons[i]._container = buttons[i].button; + this._buttons.push(buttons[i]); + this.container.appendChild(buttons[i].button); + } + + }, + + + _buildContainer: function(){ + this._container = this.container = L.DomUtil.create('div', ''); + this.options.leafletClasses && L.DomUtil.addClass(this.container, 'leaflet-bar easy-button-container leaflet-control'); + this.options.id && (this.container.id = this.options.id); + }, + + + enable: function(){ + L.DomUtil.addClass(this.container, 'enabled'); + L.DomUtil.removeClass(this.container, 'disabled'); + this.container.setAttribute('aria-hidden', 'false'); + return this; + }, + + + disable: function(){ + L.DomUtil.addClass(this.container, 'disabled'); + L.DomUtil.removeClass(this.container, 'enabled'); + this.container.setAttribute('aria-hidden', 'true'); + return this; + }, + + + onAdd: function () { + return this.container; + }, + + addTo: function (map) { + this._map = map; + + for(var i = 0; i < this._buttons.length; i++){ + this._buttons[i]._map = map; + } + + var container = this._container = this.onAdd(map), + pos = this.getPosition(), + corner = map._controlCorners[pos]; + + L.DomUtil.addClass(container, 'leaflet-control'); + + if (pos.indexOf('bottom') !== -1) { + corner.insertBefore(container, corner.firstChild); + } else { + corner.appendChild(container); + } + + return this; + } + +}); + +L.easyBar = function(){ + var args = [L.Control.EasyBar]; + for(var i = 0; i < arguments.length; i++){ + args.push( arguments[i] ); + } + return new (Function.prototype.bind.apply(L.Control.EasyBar, args)); +}; + +// L.EasyButton is the actual buttons +// can be called without being grouped into a bar +L.Control.EasyButton = L.Control.extend({ + + options: { + position: 'topleft', // part of leaflet's defaults + + id: null, // an id to tag the button with + + type: 'replace', // [(replace|animate)] + // replace swaps out elements + // animate changes classes with all elements inserted + + states: [], // state names look like this + // { + // stateName: 'untracked', + // onClick: function(){ handle_nav_manually(); }; + // title: 'click to make inactive', + // icon: 'fa-circle', // wrapped with + // } + + leafletClasses: true // use leaflet styles for the button + }, + + + + initialize: function(icon, onClick, title){ + + // clear the states manually + this.options.states = []; + + // storage between state functions + this.storage = {}; + + // is the last item an object? + if( typeof arguments[arguments.length-1] === 'object' ){ + + // if so, it should be the options + L.Util.setOptions( this, arguments[arguments.length-1] ); + } + + // if there aren't any states in options + // use the early params + if( this.options.states.length === 0 && + typeof icon === 'string' && + typeof onClick === 'function'){ + + // turn the options object into a state + this.options.states.push({ + icon: icon, + onClick: onClick, + title: typeof title === 'string' ? title : '' + }); + } + + // curate and move user's states into + // the _states for internal use + this._states = []; + + for(var i = 0; i < this.options.states.length; i++){ + this._states.push( new State(this.options.states[i], this) ); + } + + this._buildButton(); + + this._activateState(this._states[0]); + + }, + + _buildButton: function(){ + + this.button = L.DomUtil.create('button', ''); + + if (this.options.id ){ + this.button.id = this.options.id; + } + + if (this.options.leafletClasses){ + L.DomUtil.addClass(this.button, 'easy-button-button leaflet-bar-part'); + } + + // don't let double clicks get to the map + L.DomEvent.addListener(this.button, 'dblclick', L.DomEvent.stop); + + // take care of normal clicks + L.DomEvent.addListener(this.button,'click', function(e){ + L.DomEvent.stop(e); + this._currentState.onClick(this, this._map ? this._map : null ); + this._map.getContainer().focus(); + }, this); + + // prep the contents of the control + if(this.options.type == 'replace'){ + this.button.appendChild(this._currentState.icon); + } else { + for(var i=0;i"']/) ){ + + // if so, the user should have put in html + // so move forward as such + tmpIcon = ambiguousIconString; + + // then it wasn't html, so + // it's a class list, figure out what kind + } else { + ambiguousIconString = ambiguousIconString.replace(/(^\s*|\s*$)/g,''); + tmpIcon = L.DomUtil.create('span', ''); + + if( ambiguousIconString.indexOf('fa-') === 0 ){ + L.DomUtil.addClass(tmpIcon, 'fa ' + ambiguousIconString) + } else if ( ambiguousIconString.indexOf('glyphicon-') === 0 ) { + L.DomUtil.addClass(tmpIcon, 'glyphicon ' + ambiguousIconString) + } else { + L.DomUtil.addClass(tmpIcon, /*rollwithit*/ ambiguousIconString) + } + + // make this a string so that it's easy to set innerHTML below + tmpIcon = tmpIcon.outerHTML; + } + + return tmpIcon; +} + +})(); diff --git a/mapmakr/static/3rdparty/leaflet-label.css b/mapmakr/static/3rdparty/leaflet-label.css new file mode 100644 index 0000000..adedd3a --- /dev/null +++ b/mapmakr/static/3rdparty/leaflet-label.css @@ -0,0 +1,52 @@ +.leaflet-label { + background: rgb(235, 235, 235); + background: rgba(235, 235, 235, 0.8); + background-clip: padding-box; + border-color: #777; + border-color: rgba(0,0,0,0.25); + border-radius: 8px; + border-style: solid; + border-width: 1px; + color: #111; + display: block; + font: 9px/12x "Helvetica Neue", Arial, Helvetica, sans-serif; + padding: 1px 6px; + position: absolute; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + white-space: nowrap; + z-index: 6; +} + +.leaflet-label.leaflet-clickable { + cursor: pointer; +} + +.leaflet-label:before, +.leaflet-label:after { + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + content: none; + position: absolute; + top: 5px; +} + +.leaflet-label:before { + border-right: 6px solid black; + border-right-color: inherit; + left: -10px; +} + +.leaflet-label:after { + border-left: 6px solid black; + border-left-color: inherit; + right: -10px; +} + +.leaflet-label-right:before, +.leaflet-label-left:after { + content: ""; +} diff --git a/mapmakr/static/3rdparty/leaflet-label.js b/mapmakr/static/3rdparty/leaflet-label.js new file mode 100644 index 0000000..b918ca5 --- /dev/null +++ b/mapmakr/static/3rdparty/leaflet-label.js @@ -0,0 +1,9 @@ +/* + Leaflet.label, a plugin that adds labels to markers and vectors for Leaflet powered maps. + (c) 2012-2013, Jacob Toye, Smartrak + + https://github.com/Leaflet/Leaflet.label + http://leafletjs.com + https://github.com/jacobtoye +*/ +(function(){L.labelVersion="0.2.2-dev",L.Label=L.Class.extend({includes:L.Mixin.Events,options:{className:"",clickable:!1,direction:"right",noHide:!1,offset:[12,-15],opacity:1,zoomAnimation:!0},initialize:function(t,e){L.setOptions(this,t),this._source=e,this._animated=L.Browser.any3d&&this.options.zoomAnimation,this._isOpen=!1},onAdd:function(t){this._map=t,this._pane=this._source instanceof L.Marker?t._panes.markerPane:t._panes.popupPane,this._container||this._initLayout(),this._pane.appendChild(this._container),this._initInteraction(),this._update(),this.setOpacity(this.options.opacity),t.on("moveend",this._onMoveEnd,this).on("viewreset",this._onViewReset,this),this._animated&&t.on("zoomanim",this._zoomAnimation,this),L.Browser.touch&&!this.options.noHide&&L.DomEvent.on(this._container,"click",this.close,this)},onRemove:function(t){this._pane.removeChild(this._container),t.off({zoomanim:this._zoomAnimation,moveend:this._onMoveEnd,viewreset:this._onViewReset},this),this._removeInteraction(),this._map=null},setLatLng:function(t){return this._latlng=L.latLng(t),this._map&&this._updatePosition(),this},setContent:function(t){return this._previousContent=this._content,this._content=t,this._updateContent(),this},close:function(){var t=this._map;t&&(L.Browser.touch&&!this.options.noHide&&L.DomEvent.off(this._container,"click",this.close),t.removeLayer(this))},updateZIndex:function(t){this._zIndex=t,this._container&&this._zIndex&&(this._container.style.zIndex=t)},setOpacity:function(t){this.options.opacity=t,this._container&&L.DomUtil.setOpacity(this._container,t)},_initLayout:function(){this._container=L.DomUtil.create("div","leaflet-label "+this.options.className+" leaflet-zoom-animated"),this.updateZIndex(this._zIndex)},_update:function(){this._map&&(this._container.style.visibility="hidden",this._updateContent(),this._updatePosition(),this._container.style.visibility="")},_updateContent:function(){this._content&&this._map&&this._prevContent!==this._content&&"string"==typeof this._content&&(this._container.innerHTML=this._content,this._prevContent=this._content,this._labelWidth=this._container.offsetWidth)},_updatePosition:function(){var t=this._map.latLngToLayerPoint(this._latlng);this._setPosition(t)},_setPosition:function(t){var e=this._map,i=this._container,n=e.latLngToContainerPoint(e.getCenter()),o=e.layerPointToContainerPoint(t),s=this.options.direction,a=this._labelWidth,l=L.point(this.options.offset);"right"===s||"auto"===s&&o.xi;i++)L.DomEvent.on(t,e[i],this._fireMouseEvent,this)}},_removeInteraction:function(){if(this.options.clickable){var t=this._container,e=["dblclick","mousedown","mouseover","mouseout","contextmenu"];L.DomUtil.removeClass(t,"leaflet-clickable"),L.DomEvent.off(t,"click",this._onMouseClick,this);for(var i=0;e.length>i;i++)L.DomEvent.off(t,e[i],this._fireMouseEvent,this)}},_onMouseClick:function(t){this.hasEventListeners(t.type)&&L.DomEvent.stopPropagation(t),this.fire(t.type,{originalEvent:t})},_fireMouseEvent:function(t){this.fire(t.type,{originalEvent:t}),"contextmenu"===t.type&&this.hasEventListeners(t.type)&&L.DomEvent.preventDefault(t),"mousedown"!==t.type?L.DomEvent.stopPropagation(t):L.DomEvent.preventDefault(t)}}),L.BaseMarkerMethods={showLabel:function(){return this.label&&this._map&&(this.label.setLatLng(this._latlng),this._map.showLabel(this.label)),this},hideLabel:function(){return this.label&&this.label.close(),this},setLabelNoHide:function(t){this._labelNoHide!==t&&(this._labelNoHide=t,t?(this._removeLabelRevealHandlers(),this.showLabel()):(this._addLabelRevealHandlers(),this.hideLabel()))},bindLabel:function(t,e){var i=this.options.icon?this.options.icon.options.labelAnchor:this.options.labelAnchor,n=L.point(i)||L.point(0,0);return n=n.add(L.Label.prototype.options.offset),e&&e.offset&&(n=n.add(e.offset)),e=L.Util.extend({offset:n},e),this._labelNoHide=e.noHide,this.label||(this._labelNoHide||this._addLabelRevealHandlers(),this.on("remove",this.hideLabel,this).on("move",this._moveLabel,this).on("add",this._onMarkerAdd,this),this._hasLabelHandlers=!0),this.label=new L.Label(e,this).setContent(t),this},unbindLabel:function(){return this.label&&(this.hideLabel(),this.label=null,this._hasLabelHandlers&&(this._labelNoHide||this._removeLabelRevealHandlers(),this.off("remove",this.hideLabel,this).off("move",this._moveLabel,this).off("add",this._onMarkerAdd,this)),this._hasLabelHandlers=!1),this},updateLabelContent:function(t){this.label&&this.label.setContent(t)},getLabel:function(){return this.label},_onMarkerAdd:function(){this._labelNoHide&&this.showLabel()},_addLabelRevealHandlers:function(){this.on("mouseover",this.showLabel,this).on("mouseout",this.hideLabel,this),L.Browser.touch&&this.on("click",this.showLabel,this)},_removeLabelRevealHandlers:function(){this.off("mouseover",this.showLabel,this).off("mouseout",this.hideLabel,this),L.Browser.touch&&this.off("click",this.showLabel,this)},_moveLabel:function(t){this.label.setLatLng(t.latlng)}},L.Icon.Default.mergeOptions({labelAnchor:new L.Point(9,-20)}),L.Marker.mergeOptions({icon:new L.Icon.Default}),L.Marker.include(L.BaseMarkerMethods),L.Marker.include({_originalUpdateZIndex:L.Marker.prototype._updateZIndex,_updateZIndex:function(t){var e=this._zIndex+t;this._originalUpdateZIndex(t),this.label&&this.label.updateZIndex(e)},_originalSetOpacity:L.Marker.prototype.setOpacity,setOpacity:function(t,e){this.options.labelHasSemiTransparency=e,this._originalSetOpacity(t)},_originalUpdateOpacity:L.Marker.prototype._updateOpacity,_updateOpacity:function(){var t=0===this.options.opacity?0:1;this._originalUpdateOpacity(),this.label&&this.label.setOpacity(this.options.labelHasSemiTransparency?this.options.opacity:t)},_originalSetLatLng:L.Marker.prototype.setLatLng,setLatLng:function(t){return this.label&&!this._labelNoHide&&this.hideLabel(),this._originalSetLatLng(t)}}),L.CircleMarker.mergeOptions({labelAnchor:new L.Point(0,0)}),L.CircleMarker.include(L.BaseMarkerMethods),L.Path.include({bindLabel:function(t,e){return this.label&&this.label.options===e||(this.label=new L.Label(e,this)),this.label.setContent(t),this._showLabelAdded||(this.on("mouseover",this._showLabel,this).on("mousemove",this._moveLabel,this).on("mouseout remove",this._hideLabel,this),L.Browser.touch&&this.on("click",this._showLabel,this),this._showLabelAdded=!0),this},unbindLabel:function(){return this.label&&(this._hideLabel(),this.label=null,this._showLabelAdded=!1,this.off("mouseover",this._showLabel,this).off("mousemove",this._moveLabel,this).off("mouseout remove",this._hideLabel,this)),this},updateLabelContent:function(t){this.label&&this.label.setContent(t)},_showLabel:function(t){this.label.setLatLng(t.latlng),this._map.showLabel(this.label)},_moveLabel:function(t){this.label.setLatLng(t.latlng)},_hideLabel:function(){this.label.close()}}),L.Map.include({showLabel:function(t){return this.addLayer(t)}}),L.FeatureGroup.include({clearLayers:function(){return this.unbindLabel(),this.eachLayer(this.removeLayer,this),this},bindLabel:function(t,e){return this.invoke("bindLabel",t,e)},unbindLabel:function(){return this.invoke("unbindLabel")},updateLabelContent:function(t){this.invoke("updateLabelContent",t)}})})(this,document); \ No newline at end of file diff --git a/mapmakr/static/3rdparty/leaflet-search-icon.png b/mapmakr/static/3rdparty/leaflet-search-icon.png new file mode 100644 index 0000000..231df74 Binary files /dev/null and b/mapmakr/static/3rdparty/leaflet-search-icon.png differ diff --git a/mapmakr/static/3rdparty/leaflet-search-loader.gif b/mapmakr/static/3rdparty/leaflet-search-loader.gif new file mode 100644 index 0000000..d3ef195 Binary files /dev/null and b/mapmakr/static/3rdparty/leaflet-search-loader.gif differ diff --git a/mapmakr/static/3rdparty/leaflet-search.css b/mapmakr/static/3rdparty/leaflet-search.css new file mode 100644 index 0000000..7c62845 --- /dev/null +++ b/mapmakr/static/3rdparty/leaflet-search.css @@ -0,0 +1,113 @@ + +.leaflet-container .leaflet-control-search { + position:relative; + float:left; + background:#fff; + color:#1978cf; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; + background-color: rgba(255, 255, 255, 0.8); + z-index:1000; + box-shadow: 0 1px 7px rgba(0,0,0,0.65); + margin-left: 10px; + margin-top: 10px; +} +.leaflet-control-search.search-exp {/*expanded*/ + box-shadow: 0 1px 7px #999; + background: #fff; +} +.leaflet-control-search .search-input { + display:block; + float:left; + background: #fff; + border:1px solid #666; + border-radius:2px; + height:18px; + padding:0 18px 0 2px; + margin:3px 0 3px 3px; +} +.leaflet-control-search.search-load .search-input { + background: url('leaflet-search-loader.gif') no-repeat center right #fff; +} +.leaflet-control-search.search-load .search-cancel { + visibility:hidden; +} +.leaflet-control-search .search-cancel { + display:block; + width:22px; + height:18px; + position:absolute; + right:22px; + margin:3px 0; + background: url('leaflet-search-icon.png') no-repeat 0 -46px; + text-decoration:none; + filter: alpha(opacity=80); + opacity: 0.8; +} +.leaflet-control-search .search-cancel:hover { + filter: alpha(opacity=100); + opacity: 1; +} +.leaflet-control-search .search-cancel span { + display:none;/* comment for cancel button imageless */ + font-size:18px; + line-height:20px; + color:#ccc; + font-weight:bold; +} +.leaflet-control-search .search-cancel:hover span { + color:#aaa; +} +.leaflet-control-search .search-button { + display:block; + float:left; + width:26px; + height:26px; + background: url('leaflet-search-icon.png') no-repeat 2px 2px; + border-radius:4px; +} +.leaflet-control-search .search-button:hover { + background: url('leaflet-search-icon.png') no-repeat 2px -22px; +} +.leaflet-control-search .search-tooltip { + position:absolute; + top:100%; + left:0; + float:left; + min-width:120px; + max-height:122px; + box-shadow: 1px 1px 6px rgba(0,0,0,0.4); + background-color: rgba(0, 0, 0, 0.25); + z-index:1010; + overflow-y:auto; + overflow-x:hidden; +} +.leaflet-control-search .search-tip { + margin:2px; + padding:2px 4px; + display:block; + color:black; + background: #eee; + border-radius:.25em; + text-decoration:none; + white-space:nowrap; + vertical-align:center; +} +.leaflet-control-search .search-tip-select, +.leaflet-control-search .search-tip:hover, +.leaflet-control-search .search-button:hover { + background-color: #fff; +} +.leaflet-control-search .search-alert { + cursor:pointer; + clear:both; + font-size:.75em; + margin-bottom:5px; + padding:0 .25em; + color:#e00; + font-weight:bold; + border-radius:.25em; +} + + diff --git a/mapmakr/static/3rdparty/leaflet-search.js b/mapmakr/static/3rdparty/leaflet-search.js new file mode 100644 index 0000000..af81777 --- /dev/null +++ b/mapmakr/static/3rdparty/leaflet-search.js @@ -0,0 +1,934 @@ +(function() { + +L.Control.Search = L.Control.extend({ + includes: L.Mixin.Events, + // + // Name Data passed Description + // + //Managed Events: + // search_locationfound {latlng, title, layer} fired after moved and show markerLocation + // search_expanded {} fired after control was expanded + // search_collapsed {} fired after control was collapsed + // + //Public methods: + // setLayer() L.LayerGroup() set layer search at runtime + // showAlert() 'Text message' show alert message + // searchText() 'Text searched' search text by external code + // + options: { + url: '', //url for search by ajax request, ex: "search.php?q={s}". Can be function that returns string for dynamic parameter setting + layer: null, //layer where search markers(is a L.LayerGroup) + sourceData: null, //function that fill _recordsCache, passed searching text by first param and callback in second + jsonpParam: null, //jsonp param name for search by jsonp service, ex: "callback" + propertyLoc: 'loc', //field for remapping location, using array: ['latname','lonname'] for select double fields(ex. ['lat','lon'] ) support dotted format: 'prop.subprop.title' + propertyName: 'title', //property in marker.options(or feature.properties for vector layer) trough filter elements in layer, + formatData: null, //callback for reformat all data from source to indexed data object + filterData: null, //callback for filtering data from text searched, params: textSearch, allRecords + buildTip: null, //function that return row tip html node(or html string), receive text tooltip in first param + container: '', //container id to insert Search Control + minLength: 1, //minimal text length for autocomplete + initial: true, //search elements only by initial text + casesesitive: false, //search elements in case sensitive text + autoType: true, //complete input with first suggested result and select this filled-in text. + delayType: 400, //delay while typing for show tooltip + tooltipLimit: -1, //limit max results to show in tooltip. -1 for no limit. + tipAutoSubmit: true, //auto map panTo when click on tooltip + autoResize: true, //autoresize on input change + collapsed: true, //collapse search control at startup + autoCollapse: false, //collapse search control after submit(on button or on tips if enabled tipAutoSubmit) + autoCollapseTime: 1200, //delay for autoclosing alert and collapse after blur + zoom: null, //zoom after pan to location found, default: map.getZoom() + position: 'topleft', + textErr: 'Location not found', //error message + textCancel: 'Cancel', //title in cancel button + textPlaceholder: 'Search...',//placeholder value + animateLocation: true, //animate a circle over location found + circleLocation: true, //draw a circle in location found + markerLocation: false, //draw a marker in location found + markerIcon: new L.Icon.Default()//custom icon for maker location + //TODO add option for persist markerLoc after collapse! + //TODO implements uniq option 'sourceData' that recognizes source type: url,array,callback or layer + //TODO implement can do research on multiple sources layers and remote + //TODO history: false, //show latest searches in tooltip + }, +//FIXME option condition problem {autoCollapse: true, markerLocation: true} not show location +//FIXME option condition problem {autoCollapse: false } +// +//TODO important optimization!!! always append data in this._recordsCache +// now _recordsCache content is emptied and replaced with new data founded +// always appending data on _recordsCache give the possibility of caching ajax, jsonp and layersearch! +// +//TODO here insert function that search inputText FIRST in _recordsCache keys and if not find results.. +// run one of callbacks search(sourceData,jsonpUrl or options.layer) and run this.showTooltip +// +//TODO change structure of _recordsCache +// like this: _recordsCache = {"text-key1": {loc:[lat,lng], ..other attributes.. }, {"text-key2": {loc:[lat,lng]}...}, ...} +// in this mode every record can have a free structure of attributes, only 'loc' is required + + initialize: function(options) { + L.Util.setOptions(this, options || {}); + this._inputMinSize = this.options.textPlaceholder ? this.options.textPlaceholder.length : 10; + this._layer = this.options.layer || new L.LayerGroup(); + this._filterData = this.options.filterData || this._defaultFilterData; + this._formatData = this.options.formatData || this._defaultFormatData; + this._autoTypeTmp = this.options.autoType; //useful for disable autoType temporarily in delete/backspace keydown + this._countertips = 0; //number of tips items + this._recordsCache = {}; //key,value table! that store locations! format: key,latlng + this._curReq = null; + }, + + onAdd: function (map) { + this._map = map; + this._container = L.DomUtil.create('div', 'leaflet-control-search'); + this._input = this._createInput(this.options.textPlaceholder, 'search-input'); + this._tooltip = this._createTooltip('search-tooltip'); + this._cancel = this._createCancel(this.options.textCancel, 'search-cancel'); + this._button = this._createButton(this.options.textPlaceholder, 'search-button'); + this._alert = this._createAlert('search-alert'); + + if(this.options.collapsed===false) + this.expand(this.options.collapsed); + + if(this.options.circleLocation || this.options.markerLocation || this.options.markerIcon) + this._markerLoc = new L.Control.Search.Marker([0,0], { + showCircle: this.options.circleLocation, + showMarker: this.options.markerLocation, + icon: this.options.markerIcon + });//see below + + this.setLayer( this._layer ); + map.on({ + // 'layeradd': this._onLayerAddRemove, + // 'layerremove': this._onLayerAddRemove + 'resize': this._handleAutoresize + }, this); + return this._container; + }, + addTo: function (map) { + + if(this.options.container) { + this._container = this.onAdd(map); + this._wrapper = L.DomUtil.get(this.options.container); + this._wrapper.style.position = 'relative'; + this._wrapper.appendChild(this._container); + } + else + L.Control.prototype.addTo.call(this, map); + + return this; + }, + + onRemove: function(map) { + this._recordsCache = {}; + // map.off({ + // 'layeradd': this._onLayerAddRemove, + // 'layerremove': this._onLayerAddRemove + // }, this); + }, + + // _onLayerAddRemove: function(e) { + // //console.info('_onLayerAddRemove'); + // //without this, run setLayer also for each Markers!! to optimize! + // if(e.layer instanceof L.LayerGroup) + // if( L.stamp(e.layer) != L.stamp(this._layer) ) + // this.setLayer(e.layer); + // }, + + _getPath: function(obj, prop) { + var parts = prop.split('.'), + last = parts.pop(), + len = parts.length, + cur = parts[0], + i = 1; + + if(len > 0) + while((obj = obj[cur]) && i < len) + cur = parts[i++]; + + if(obj) + return obj[last]; + }, + + setLayer: function(layer) { //set search layer at runtime + //this.options.layer = layer; //setting this, run only this._recordsFromLayer() + this._layer = layer; + this._layer.addTo(this._map); + if(this._markerLoc) + this._layer.addLayer(this._markerLoc); + return this; + }, + + showAlert: function(text) { + text = text || this.options.textErr; + this._alert.style.display = 'block'; + this._alert.innerHTML = text; + clearTimeout(this.timerAlert); + var that = this; + this.timerAlert = setTimeout(function() { + that.hideAlert(); + },this.options.autoCollapseTime); + return this; + }, + + hideAlert: function() { + this._alert.style.display = 'none'; + return this; + }, + + cancel: function() { + this._input.value = ''; + this._handleKeypress({keyCode:8});//simulate backspace keypress + this._input.size = this._inputMinSize; + this._input.focus(); + this._cancel.style.display = 'none'; + this._hideTooltip(); + return this; + }, + + expand: function(toggle) { + toggle = typeof toggle === 'boolean' ? toggle : true; + this._input.style.display = 'block'; + L.DomUtil.addClass(this._container, 'search-exp'); + if ( toggle !== false ) { + this._input.focus(); + this._map.on('dragstart click', this.collapse, this); + } + this.fire('search_expanded'); + return this; + }, + + collapse: function() { + this._hideTooltip(); + this.cancel(); + this._alert.style.display = 'none'; + this._input.blur(); + if(this.options.collapsed) + { + this._input.style.display = 'none'; + this._cancel.style.display = 'none'; + L.DomUtil.removeClass(this._container, 'search-exp'); + //this._markerLoc.hide();//maybe unuseful + this._map.off('dragstart click', this.collapse, this); + } + this.fire('search_collapsed'); + return this; + }, + + collapseDelayed: function() { //collapse after delay, used on_input blur + if (!this.options.autoCollapse) return this; + var that = this; + clearTimeout(this.timerCollapse); + this.timerCollapse = setTimeout(function() { + that.collapse(); + }, this.options.autoCollapseTime); + return this; + }, + + collapseDelayedStop: function() { + clearTimeout(this.timerCollapse); + return this; + }, + +////start DOM creations + _createAlert: function(className) { + var alert = L.DomUtil.create('div', className, this._container); + alert.style.display = 'none'; + + L.DomEvent + .on(alert, 'click', L.DomEvent.stop, this) + .on(alert, 'click', this.hideAlert, this); + + return alert; + }, + + _createInput: function (text, className) { + var label = L.DomUtil.create('label', className, this._container); + var input = L.DomUtil.create('input', className, this._container); + input.type = 'text'; + input.size = this._inputMinSize; + input.value = ''; + input.autocomplete = 'off'; + input.autocorrect = 'off'; + input.autocapitalize = 'off'; + input.placeholder = text; + input.style.display = 'none'; + input.role = 'search'; + input.id = input.role + input.type + input.size; + + label.htmlFor = input.id; + label.style.display = 'none'; + label.value = text; + + L.DomEvent + .disableClickPropagation(input) + .on(input, 'keyup', this._handleKeypress, this) + .on(input, 'keydown', this._handleAutoresize, this) + .on(input, 'blur', this.collapseDelayed, this) + .on(input, 'focus', this.collapseDelayedStop, this); + + return input; + }, + + _createCancel: function (title, className) { + var cancel = L.DomUtil.create('a', className, this._container); + cancel.href = '#'; + cancel.title = title; + cancel.style.display = 'none'; + cancel.innerHTML = "";//imageless(see css) + + L.DomEvent + .on(cancel, 'click', L.DomEvent.stop, this) + .on(cancel, 'click', this.cancel, this); + + return cancel; + }, + + _createButton: function (title, className) { + var button = L.DomUtil.create('a', className, this._container); + button.href = '#'; + button.title = title; + + L.DomEvent + .on(button, 'click', L.DomEvent.stop, this) + .on(button, 'click', this._handleSubmit, this) + .on(button, 'focus', this.collapseDelayedStop, this) + .on(button, 'blur', this.collapseDelayed, this); + + return button; + }, + + _createTooltip: function(className) { + var tool = L.DomUtil.create('ul', className, this._container); + tool.style.display = 'none'; + + var that = this; + L.DomEvent + .disableClickPropagation(tool) + .on(tool, 'blur', this.collapseDelayed, this) + .on(tool, 'mousewheel', function(e) { + that.collapseDelayedStop(); + L.DomEvent.stopPropagation(e);//disable zoom map + }, this) + .on(tool, 'mouseover', function(e) { + that.collapseDelayedStop(); + }, this); + return tool; + }, + + _createTip: function(text, val) {//val is object in recordCache, usually is Latlng + var tip; + + if(this.options.buildTip) + { + tip = this.options.buildTip.call(this, text, val); //custom tip node or html string + if(typeof tip === 'string') + { + var tmpNode = L.DomUtil.create('div'); + tmpNode.innerHTML = tip; + tip = tmpNode.firstChild; + } + } + else + { + tip = L.DomUtil.create('li', ''); + tip.innerHTML = text; + } + + L.DomUtil.addClass(tip, 'search-tip'); + tip._text = text; //value replaced in this._input and used by _autoType + + if(this.options.tipAutoSubmit) + L.DomEvent + .disableClickPropagation(tip) + .on(tip, 'click', L.DomEvent.stop, this) + .on(tip, 'click', function(e) { + this._input.value = text; + this._handleAutoresize(); + this._input.focus(); + this._hideTooltip(); + this._handleSubmit(); + }, this); + + return tip; + }, + +//////end DOM creations + + _getUrl: function(text) { + return (typeof this.options.url === 'function') ? this.options.url(text) : this.options.url; + }, + + _defaultFilterData: function(text, records) { + + var regFilter = new RegExp("^[.]$|[\[\]|()*]",'g'), //remove . * | ( ) ] [ + I, regSearch, + frecords = {}; + + text = text.replace(regFilter,''); //sanitize text + I = this.options.initial ? '^' : ''; //search only initial text + + regSearch = new RegExp(I + text, !this.options.casesesitive ? 'i' : undefined); + + //TODO use .filter or .map + for(var key in records) + if( regSearch.test(key) ) + frecords[key]= records[key]; + + return frecords; + }, + + showTooltip: function(records) { + var tip; + + this._countertips = 0; + + this._tooltip.innerHTML = ''; + this._tooltip.currentSelection = -1; //inizialized for _handleArrowSelect() + + for(var key in records)//fill tooltip + { + if(++this._countertips == this.options.tooltipLimit) break; + + tip = this._createTip(key, records[key] ); + + this._tooltip.appendChild(tip); + } + + if(this._countertips > 0) + { + this._tooltip.style.display = 'block'; + if(this._autoTypeTmp) + this._autoType(); + this._autoTypeTmp = this.options.autoType;//reset default value + } + else + this._hideTooltip(); + + this._tooltip.scrollTop = 0; + return this._countertips; + }, + + _hideTooltip: function() { + this._tooltip.style.display = 'none'; + this._tooltip.innerHTML = ''; + return 0; + }, + + _defaultFormatData: function(json) { //default callback for format data to indexed data + var propName = this.options.propertyName, + propLoc = this.options.propertyLoc, + i, jsonret = {}; + + if( L.Util.isArray(propLoc) ) + for(i in json) + jsonret[ this._getPath(json[i],propName) ]= L.latLng( json[i][ propLoc[0] ], json[i][ propLoc[1] ] ); + else + for(i in json) + jsonret[ this._getPath(json[i],propName) ]= L.latLng( this._getPath(json[i],propLoc) ); + //TODO throw new Error("propertyName '"+propName+"' not found in JSON data"); + return jsonret; + }, + + _recordsFromJsonp: function(text, callAfter) { //extract searched records from remote jsonp service + L.Control.Search.callJsonp = callAfter; + var script = L.DomUtil.create('script','leaflet-search-jsonp', document.getElementsByTagName('body')[0] ), + url = L.Util.template(this._getUrl(text)+'&'+this.options.jsonpParam+'=L.Control.Search.callJsonp', {s: text}); //parsing url + //rnd = '&_='+Math.floor(Math.random()*10000); + //TODO add rnd param or randomize callback name! in recordsFromJsonp + script.type = 'text/javascript'; + script.src = url; + return { abort: function() { script.parentNode.removeChild(script); } }; + }, + + _recordsFromAjax: function(text, callAfter) { //Ajax request + if (window.XMLHttpRequest === undefined) { + window.XMLHttpRequest = function() { + try { return new ActiveXObject("Microsoft.XMLHTTP.6.0"); } + catch (e1) { + try { return new ActiveXObject("Microsoft.XMLHTTP.3.0"); } + catch (e2) { throw new Error("XMLHttpRequest is not supported"); } + } + }; + } + var IE8or9 = ( L.Browser.ie && !window.atob && document.querySelector ), + request = IE8or9 ? new XDomainRequest() : new XMLHttpRequest(), + url = L.Util.template(this._getUrl(text), {s: text}); + + //rnd = '&_='+Math.floor(Math.random()*10000); + //TODO add rnd param or randomize callback name! in recordsFromAjax + + request.open("GET", url); + var that = this; + + request.onload = function() { + callAfter( JSON.parse(request.responseText) ); + }; + request.onreadystatechange = function() { + if(request.readyState === 4 && request.status === 200) { + this.onload(); + } + }; + + request.send(); + return request; + }, + + _recordsFromLayer: function() { //return table: key,value from layer + var that = this, + retRecords = {}, + propName = this.options.propertyName, + loc; + + this._layer.eachLayer(function(layer) { + + if(layer instanceof L.Control.Search.Marker) return; + + if(layer instanceof L.Marker || layer instanceof L.CircleMarker) + { + if(that._getPath(layer.options,propName)) + { + loc = layer.getLatLng(); + loc.layer = layer; + retRecords[ that._getPath(layer.options,propName) ] = loc; + + } + else if(that._getPath(layer.feature.properties,propName)){ + + loc = layer.getLatLng(); + loc.layer = layer; + retRecords[ that._getPath(layer.feature.properties,propName) ] = loc; + + } + else + throw new Error("propertyName '"+propName+"' not found in marker"); + } + else if(layer.hasOwnProperty('feature'))//GeoJSON + { + if(layer.feature.properties.hasOwnProperty(propName)) + { + loc = layer.getBounds().getCenter(); + loc.layer = layer; + retRecords[ layer.feature.properties[propName] ] = loc; + } + else + throw new Error("propertyName '"+propName+"' not found in feature"); + } + else if(layer instanceof L.LayerGroup) + { + //TODO: Optimize + layer.eachLayer(function(m) { + loc = m.getLatLng(); + loc.layer = m; + retRecords[ m.feature.properties[propName] ] = loc; + }); + } + + },this); + + return retRecords; + }, + + _autoType: function() { + + //TODO implements autype without selection(useful for mobile device) + + var start = this._input.value.length, + firstRecord = this._tooltip.firstChild._text, + end = firstRecord.length; + + if (firstRecord.indexOf(this._input.value) === 0) { // If prefix match + this._input.value = firstRecord; + this._handleAutoresize(); + + if (this._input.createTextRange) { + var selRange = this._input.createTextRange(); + selRange.collapse(true); + selRange.moveStart('character', start); + selRange.moveEnd('character', end); + selRange.select(); + } + else if(this._input.setSelectionRange) { + this._input.setSelectionRange(start, end); + } + else if(this._input.selectionStart) { + this._input.selectionStart = start; + this._input.selectionEnd = end; + } + } + }, + + _hideAutoType: function() { // deselect text: + + var sel; + if ((sel = this._input.selection) && sel.empty) { + sel.empty(); + } + else if (this._input.createTextRange) { + sel = this._input.createTextRange(); + sel.collapse(true); + var end = this._input.value.length; + sel.moveStart('character', end); + sel.moveEnd('character', end); + sel.select(); + } + else { + if (this._input.getSelection) { + this._input.getSelection().removeAllRanges(); + } + this._input.selectionStart = this._input.selectionEnd; + } + }, + + _handleKeypress: function (e) { //run _input keyup event + + switch(e.keyCode) + { + case 27: //Esc + this.collapse(); + break; + case 13: //Enter + if(this._countertips == 1) + this._handleArrowSelect(1); + this._handleSubmit(); //do search + break; + case 38://Up + this._handleArrowSelect(-1); + break; + case 40://Down + this._handleArrowSelect(1); + break; + case 37://Left + case 39://Right + case 16://Shift + case 17://Ctrl + //case 32://Space + break; + case 8://backspace + case 46://delete + this._autoTypeTmp = false;//disable temporarily autoType + break; + default://All keys + + if(this._input.value.length) + this._cancel.style.display = 'block'; + else + this._cancel.style.display = 'none'; + + if(this._input.value.length >= this.options.minLength) + { + var that = this; + + clearTimeout(this.timerKeypress); //cancel last search request while type in + this.timerKeypress = setTimeout(function() { //delay before request, for limit jsonp/ajax request + + that._fillRecordsCache(); + + }, this.options.delayType); + } + else + this._hideTooltip(); + } + }, + + searchText: function(text) { + var code = text.charCodeAt(text.length); + + this._input.value = text; + + this._input.style.display = 'block'; + L.DomUtil.addClass(this._container, 'search-exp'); + + this._autoTypeTmp = false; + + this._handleKeypress({keyCode: code}); + }, + + _fillRecordsCache: function() { +//TODO important optimization!!! always append data in this._recordsCache +// now _recordsCache content is emptied and replaced with new data founded +// always appending data on _recordsCache give the possibility of caching ajax, jsonp and layersearch! +// +//TODO here insert function that search inputText FIRST in _recordsCache keys and if not find results.. +// run one of callbacks search(sourceData,jsonpUrl or options.layer) and run this.showTooltip +// +//TODO change structure of _recordsCache +// like this: _recordsCache = {"text-key1": {loc:[lat,lng], ..other attributes.. }, {"text-key2": {loc:[lat,lng]}...}, ...} +// in this way every record can have a free structure of attributes, only 'loc' is required + + var inputText = this._input.value, + that = this, records; + + if(this._curReq && this._curReq.abort) + this._curReq.abort(); + //abort previous requests + + L.DomUtil.addClass(this._container, 'search-load'); + + if(this.options.layer) + { + //TODO _recordsFromLayer must return array of objects, formatted from _formatData + this._recordsCache = this._recordsFromLayer(); + + records = this._filterData( this._input.value, this._recordsCache ); + + this.showTooltip( records ); + + L.DomUtil.removeClass(this._container, 'search-load'); + } + else + { + if(this.options.sourceData) + this._retrieveData = this.options.sourceData; + + else if(this.options.url) //jsonp or ajax + this._retrieveData = this.options.jsonpParam ? this._recordsFromJsonp : this._recordsFromAjax; + + this._curReq = this._retrieveData.call(this, inputText, function(data) { + + that._recordsCache = that._formatData(data); + + //TODO refact! + if(that.options.sourceData) + records = that._filterData( that._input.value, that._recordsCache ); + else + records = that._recordsCache; + + that.showTooltip( records ); + + L.DomUtil.removeClass(that._container, 'search-load'); + }); + } + }, + + _handleAutoresize: function() { //autoresize this._input + //TODO refact _handleAutoresize now is not accurate + if (this._input.style.maxWidth != this._map._container.offsetWidth) //If maxWidth isn't the same as when first set, reset to current Map width + this._input.style.maxWidth = L.DomUtil.getStyle(this._map._container, 'width'); + + if(this.options.autoResize && (this._container.offsetWidth + 45 < this._map._container.offsetWidth)) + this._input.size = this._input.value.length= (searchTips.length - 1))) {// If at end of list. + L.DomUtil.addClass(searchTips[this._tooltip.currentSelection], 'search-tip-select'); + } + else if ((velocity == -1 ) && (this._tooltip.currentSelection <= 0)) { // Going back up to the search box. + this._tooltip.currentSelection = -1; + } + else if (this._tooltip.style.display != 'none') { + this._tooltip.currentSelection += velocity; + + L.DomUtil.addClass(searchTips[this._tooltip.currentSelection], 'search-tip-select'); + + this._input.value = searchTips[this._tooltip.currentSelection]._text; + + // scroll: + var tipOffsetTop = searchTips[this._tooltip.currentSelection].offsetTop; + + if (tipOffsetTop + searchTips[this._tooltip.currentSelection].clientHeight >= this._tooltip.scrollTop + this._tooltip.clientHeight) { + this._tooltip.scrollTop = tipOffsetTop - this._tooltip.clientHeight + searchTips[this._tooltip.currentSelection].clientHeight; + } + else if (tipOffsetTop <= this._tooltip.scrollTop) { + this._tooltip.scrollTop = tipOffsetTop; + } + } + }, + + _handleSubmit: function() { //button and tooltip click and enter submit + + this._hideAutoType(); + + this.hideAlert(); + this._hideTooltip(); + + if(this._input.style.display == 'none') //on first click show _input only + this.expand(); + else + { + if(this._input.value === '') //hide _input only + this.collapse(); + else + { + var loc = this._getLocation(this._input.value); + + if(loc===false) + this.showAlert(); + else + { + this.showLocation(loc, this._input.value); + this.fire('search_locationfound', { + latlng: loc, + text: this._input.value, + layer: loc.layer ? loc.layer : null + }); + } + //this.collapse(); + //FIXME if collapse in _handleSubmit hide _markerLoc! + } + } + }, + + _getLocation: function(key) { //extract latlng from _recordsCache + + if( this._recordsCache.hasOwnProperty(key) ) + return this._recordsCache[key];//then after use .loc attribute + else + return false; + }, + + showLocation: function(latlng, title) { //set location on map from _recordsCache + + if(this.options.zoom) + this._map.setView(latlng, this.options.zoom); + else + this._map.panTo(latlng); + + if(this._markerLoc) + { + this._markerLoc.setLatLng(latlng); //show circle/marker in location found + this._markerLoc.setTitle(title); + this._markerLoc.show(); + if(this.options.animateLocation) + this._markerLoc.animate(); + //TODO showLocation: start animation after setView or panTo, maybe with map.on('moveend')... + } + + //FIXME autoCollapse option hide this._markerLoc before that visualized!! + if(this.options.autoCollapse) + this.collapse(); + return this; + } +}); + +L.Control.Search.Marker = L.Marker.extend({ + + includes: L.Mixin.Events, + + options: { + radius: 10, + weight: 3, + color: '#e03', + stroke: true, + fill: false, + title: '', + icon: new L.Icon.Default(), + showCircle: true, + showMarker: false //show icon optional, show only circleLoc + }, + + initialize: function (latlng, options) { + L.setOptions(this, options); + L.Marker.prototype.initialize.call(this, latlng, options); + if(this.options.showCircle) + this._circleLoc = new L.CircleMarker(latlng, this.options); + }, + + onAdd: function (map) { + L.Marker.prototype.onAdd.call(this, map); + if(this._circleLoc) + map.addLayer(this._circleLoc); + this.hide(); + }, + + onRemove: function (map) { + L.Marker.prototype.onRemove.call(this, map); + if(this._circleLoc) + map.removeLayer(this._circleLoc); + }, + + setLatLng: function (latlng) { + L.Marker.prototype.setLatLng.call(this, latlng); + if(this._circleLoc) + this._circleLoc.setLatLng(latlng); + return this; + }, + + setTitle: function(title) { + title = title || ''; + this.options.title = title; + if(this._icon) + this._icon.title = title; + return this; + }, + + show: function() { + if(this.options.showMarker) + { + if(this._icon) + this._icon.style.display = 'block'; + if(this._shadow) + this._shadow.style.display = 'block'; + //this._bringToFront(); + } + if(this._circleLoc) + { + this._circleLoc.setStyle({fill: this.options.fill, stroke: this.options.stroke}); + //this._circleLoc.bringToFront(); + } + return this; + }, + + hide: function() { + if(this._icon) + this._icon.style.display = 'none'; + if(this._shadow) + this._shadow.style.display = 'none'; + if(this._circleLoc) + this._circleLoc.setStyle({fill: false, stroke: false}); + return this; + }, + + animate: function() { + //TODO refact animate() more smooth! like this: http://goo.gl/DDlRs + if(this._circleLoc) + { + var circle = this._circleLoc, + tInt = 200, //time interval + ss = 10, //frames + mr = parseInt(circle._radius/ss), + oldrad = this.options.radius, + newrad = circle._radius * 2.5, + acc = 0; + + circle._timerAnimLoc = setInterval(function() { + acc += 0.5; + mr += acc; //adding acceleration + newrad -= mr; + + circle.setRadius(newrad); + + if(newrad=s;)n=n.__parent;return this._currentShownBounds.contains(n.getLatLng())&&(this.options.animateAddingMarkers?this._animationAddLayer(t,n):this._animationAddLayerNonAnimated(t,n)),this},removeLayer:function(t){if(t instanceof L.LayerGroup){var e=[];for(var i in t._layers)e.push(t._layers[i]);return this.removeLayers(e)}return t.getLatLng?this._map?t.__parent?(this._unspiderfy&&(this._unspiderfy(),this._unspiderfyLayer(t)),this._removeLayer(t,!0),this._topClusterLevel._recalculateBounds(),this._featureGroup.hasLayer(t)&&(this._featureGroup.removeLayer(t),t.clusterShow&&t.clusterShow()),this):this:(!this._arraySplice(this._needsClustering,t)&&this.hasLayer(t)&&this._needsRemoving.push(t),this):(this._nonPointGroup.removeLayer(t),this)},addLayers:function(t){var e,i,n,s,r=this._featureGroup,o=this._nonPointGroup,a=this.options.chunkedLoading,h=this.options.chunkInterval,u=this.options.chunkProgress;if(this._map){var _=0,l=(new Date).getTime(),d=L.bind(function(){for(var e=(new Date).getTime();_h)break}if(s=t[_],s.getLatLng){if(!this.hasLayer(s)&&(this._addLayer(s,this._maxZoom),s.__parent&&2===s.__parent.getChildCount())){var n=s.__parent.getAllChildMarkers(),c=n[0]===s?n[1]:n[0];r.removeLayer(c)}}else o.addLayer(s)}u&&u(_,t.length,(new Date).getTime()-l),_===t.length?(this._topClusterLevel._recalculateBounds(),this._featureGroup.eachLayer(function(t){t instanceof L.MarkerCluster&&t._iconNeedsUpdate&&t._updateIcon()}),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds)):setTimeout(d,this.options.chunkDelay)},this);d()}else{for(e=[],i=0,n=t.length;n>i;i++)s=t[i],s.getLatLng?this.hasLayer(s)||e.push(s):o.addLayer(s);this._needsClustering=this._needsClustering.concat(e)}return this},removeLayers:function(t){var e,i,n,s=this._featureGroup,r=this._nonPointGroup;if(!this._map){for(e=0,i=t.length;i>e;e++)n=t[e],this._arraySplice(this._needsClustering,n),r.removeLayer(n),this.hasLayer(n)&&this._needsRemoving.push(n);return this}if(this._unspiderfy)for(this._unspiderfy(),e=0,i=t.length;i>e;e++)n=t[e],this._unspiderfyLayer(n);for(e=0,i=t.length;i>e;e++)n=t[e],n.__parent?(this._removeLayer(n,!0,!0),s.hasLayer(n)&&(s.removeLayer(n),n.clusterShow&&n.clusterShow())):r.removeLayer(n);return this._topClusterLevel._recalculateBounds(),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._zoom,this._currentShownBounds),s.eachLayer(function(t){t instanceof L.MarkerCluster&&t._updateIcon()}),this},clearLayers:function(){return this._map||(this._needsClustering=[],delete this._gridClusters,delete this._gridUnclustered),this._noanimationUnspiderfy&&this._noanimationUnspiderfy(),this._featureGroup.clearLayers(),this._nonPointGroup.clearLayers(),this.eachLayer(function(t){delete t.__parent}),this._map&&this._generateInitialClusters(),this},getBounds:function(){var t=new L.LatLngBounds;this._topClusterLevel&&t.extend(this._topClusterLevel._bounds);for(var e=this._needsClustering.length-1;e>=0;e--)t.extend(this._needsClustering[e].getLatLng());return t.extend(this._nonPointGroup.getBounds()),t},eachLayer:function(t,e){var i,n=this._needsClustering.slice();for(this._topClusterLevel&&this._topClusterLevel.getAllChildMarkers(n),i=n.length-1;i>=0;i--)t.call(e,n[i]);this._nonPointGroup.eachLayer(t,e)},getLayers:function(){var t=[];return this.eachLayer(function(e){t.push(e)}),t},getLayer:function(t){var e=null;return t=parseInt(t,10),this.eachLayer(function(i){L.stamp(i)===t&&(e=i)}),e},hasLayer:function(t){if(!t)return!1;var e,i=this._needsClustering;for(e=i.length-1;e>=0;e--)if(i[e]===t)return!0;for(i=this._needsRemoving,e=i.length-1;e>=0;e--)if(i[e]===t)return!1;return!(!t.__parent||t.__parent._group!==this)||this._nonPointGroup.hasLayer(t)},zoomToShowLayer:function(t,e){"function"!=typeof e&&(e=function(){});var i=function(){!t._icon&&!t.__parent._icon||this._inZoomAnimation||(this._map.off("moveend",i,this),this.off("animationend",i,this),t._icon?e():t.__parent._icon&&(this.once("spiderfied",e,this),t.__parent.spiderfy()))};if(t._icon&&this._map.getBounds().contains(t.getLatLng()))e();else if(t.__parent._zoome;e++)n=this._needsRemoving[e],this._removeLayer(n,!0);this._needsRemoving=[],this._zoom=this._map.getZoom(),this._currentShownBounds=this._getExpandedVisibleBounds(),this._map.on("zoomend",this._zoomEnd,this),this._map.on("moveend",this._moveEnd,this),this._spiderfierOnAdd&&this._spiderfierOnAdd(),this._bindEvents(),i=this._needsClustering,this._needsClustering=[],this.addLayers(i)},onRemove:function(t){t.off("zoomend",this._zoomEnd,this),t.off("moveend",this._moveEnd,this),this._unbindEvents(),this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim",""),this._spiderfierOnRemove&&this._spiderfierOnRemove(),delete this._maxLat,this._hideCoverage(),this._featureGroup.onRemove(t),this._nonPointGroup.onRemove(t),this._featureGroup.clearLayers(),this._map=null},getVisibleParent:function(t){for(var e=t;e&&!e._icon;)e=e.__parent;return e||null},_arraySplice:function(t,e){for(var i=t.length-1;i>=0;i--)if(t[i]===e)return t.splice(i,1),!0},_removeFromGridUnclustered:function(t,e){for(var i=this._map,n=this._gridUnclustered;e>=0&&n[e].removeObject(t,i.project(t.getLatLng(),e));e--);},_removeLayer:function(t,e,i){var n=this._gridClusters,s=this._gridUnclustered,r=this._featureGroup,o=this._map;e&&this._removeFromGridUnclustered(t,this._maxZoom);var a,h=t.__parent,u=h._markers;for(this._arraySplice(u,t);h&&(h._childCount--,h._boundsNeedUpdate=!0,!(h._zoom<0));)e&&h._childCount<=1?(a=h._markers[0]===t?h._markers[1]:h._markers[0],n[h._zoom].removeObject(h,o.project(h._cLatLng,h._zoom)),s[h._zoom].addObject(a,o.project(a.getLatLng(),h._zoom)),this._arraySplice(h.__parent._childClusters,h),h.__parent._markers.push(a),a.__parent=h.__parent,h._icon&&(r.removeLayer(h),i||r.addLayer(a))):i&&h._icon||h._updateIcon(),h=h.__parent;delete t.__parent},_isOrIsParent:function(t,e){for(;e;){if(t===e)return!0;e=e.parentNode}return!1},_propagateEvent:function(t){if(t.layer instanceof L.MarkerCluster){if(t.originalEvent&&this._isOrIsParent(t.layer._icon,t.originalEvent.relatedTarget))return;t.type="cluster"+t.type}this.fire(t.type,t)},_defaultIconCreateFunction:function(t){var e=t.getChildCount(),i=" marker-cluster-";return i+=10>e?"small":100>e?"medium":"large",new L.DivIcon({html:"
"+e+"
",className:"marker-cluster"+i,iconSize:new L.Point(40,40)})},_bindEvents:function(){var t=this._map,e=this.options.spiderfyOnMaxZoom,i=this.options.showCoverageOnHover,n=this.options.zoomToBoundsOnClick;(e||n)&&this.on("clusterclick",this._zoomOrSpiderfy,this),i&&(this.on("clustermouseover",this._showCoverage,this),this.on("clustermouseout",this._hideCoverage,this),t.on("zoomend",this._hideCoverage,this))},_zoomOrSpiderfy:function(t){for(var e=t.layer,i=e;1===i._childClusters.length;)i=i._childClusters[0];i._zoom===this._maxZoom&&i._childCount===e._childCount?this.options.spiderfyOnMaxZoom&&e.spiderfy():this.options.zoomToBoundsOnClick&&e.zoomToBounds(),t.originalEvent&&13===t.originalEvent.keyCode&&this._map._container.focus()},_showCoverage:function(t){var e=this._map;this._inZoomAnimation||(this._shownPolygon&&e.removeLayer(this._shownPolygon),t.layer.getChildCount()>2&&t.layer!==this._spiderfied&&(this._shownPolygon=new L.Polygon(t.layer.getConvexHull(),this.options.polygonOptions),e.addLayer(this._shownPolygon)))},_hideCoverage:function(){this._shownPolygon&&(this._map.removeLayer(this._shownPolygon),this._shownPolygon=null)},_unbindEvents:function(){var t=this.options.spiderfyOnMaxZoom,e=this.options.showCoverageOnHover,i=this.options.zoomToBoundsOnClick,n=this._map;(t||i)&&this.off("clusterclick",this._zoomOrSpiderfy,this),e&&(this.off("clustermouseover",this._showCoverage,this),this.off("clustermouseout",this._hideCoverage,this),n.off("zoomend",this._hideCoverage,this))},_zoomEnd:function(){this._map&&(this._mergeSplitClusters(),this._zoom=this._map._zoom,this._currentShownBounds=this._getExpandedVisibleBounds())},_moveEnd:function(){if(!this._inZoomAnimation){var t=this._getExpandedVisibleBounds();this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,this._zoom,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,this._map._zoom,t),this._currentShownBounds=t}},_generateInitialClusters:function(){var t=this._map.getMaxZoom(),e=this.options.maxClusterRadius,i=e;"function"!=typeof e&&(i=function(){return e}),this.options.disableClusteringAtZoom&&(t=this.options.disableClusteringAtZoom-1),this._maxZoom=t,this._gridClusters={},this._gridUnclustered={};for(var n=t;n>=0;n--)this._gridClusters[n]=new L.DistanceGrid(i(n)),this._gridUnclustered[n]=new L.DistanceGrid(i(n));this._topClusterLevel=new this._markerCluster(this,-1)},_addLayer:function(t,e){var i,n,s=this._gridClusters,r=this._gridUnclustered;for(this.options.singleMarkerMode&&this._overrideMarkerIcon(t);e>=0;e--){i=this._map.project(t.getLatLng(),e);var o=s[e].getNearObject(i);if(o)return o._addChild(t),t.__parent=o,void 0;if(o=r[e].getNearObject(i)){var a=o.__parent;a&&this._removeLayer(o,!1);var h=new this._markerCluster(this,e,o,t);s[e].addObject(h,this._map.project(h._cLatLng,e)),o.__parent=h,t.__parent=h;var u=h;for(n=e-1;n>a._zoom;n--)u=new this._markerCluster(this,n,u),s[n].addObject(u,this._map.project(o.getLatLng(),n));return a._addChild(u),this._removeFromGridUnclustered(o,e),void 0}r[e].addObject(t,i)}this._topClusterLevel._addChild(t),t.__parent=this._topClusterLevel},_enqueue:function(t){this._queue.push(t),this._queueTimeout||(this._queueTimeout=setTimeout(L.bind(this._processQueue,this),300))},_processQueue:function(){for(var t=0;tthis._map._zoom?(this._animationStart(),this._animationZoomOut(this._zoom,this._map._zoom)):this._moveEnd()},_getExpandedVisibleBounds:function(){return this.options.removeOutsideVisibleBounds?L.Browser.mobile?this._checkBoundsMaxLat(this._map.getBounds()):this._checkBoundsMaxLat(this._map.getBounds().pad(1)):this._mapBoundsInfinite},_checkBoundsMaxLat:function(t){var e=this._maxLat;return e!==i&&(t.getNorth()>=e&&(t._northEast.lat=1/0),t.getSouth()<=-e&&(t._southWest.lat=-1/0)),t},_animationAddLayerNonAnimated:function(t,e){if(e===t)this._featureGroup.addLayer(t);else if(2===e._childCount){e._addToMap();var i=e.getAllChildMarkers();this._featureGroup.removeLayer(i[0]),this._featureGroup.removeLayer(i[1])}else e._updateIcon()},_overrideMarkerIcon:function(t){var e=t.options.icon=this.options.iconCreateFunction({getChildCount:function(){return 1},getAllChildMarkers:function(){return[t]}});return e}}),L.MarkerClusterGroup.include({_mapBoundsInfinite:new L.LatLngBounds(new L.LatLng(-1/0,-1/0),new L.LatLng(1/0,1/0))}),L.MarkerClusterGroup.include({_noAnimation:{_animationStart:function(){},_animationZoomIn:function(t,e){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds()),this.fire("animationend")},_animationZoomOut:function(t,e){this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds()),this.fire("animationend")},_animationAddLayer:function(t,e){this._animationAddLayerNonAnimated(t,e)}},_withAnimation:{_animationStart:function(){this._map._mapPane.className+=" leaflet-cluster-anim",this._inZoomAnimation++},_animationZoomIn:function(t,e){var i,n=this._getExpandedVisibleBounds(),s=this._featureGroup;this._topClusterLevel._recursively(n,t,0,function(r){var o,a=r._latlng,h=r._markers;for(n.contains(a)||(a=null),r._isSingleParent()&&t+1===e?(s.removeLayer(r),r._recursivelyAddChildrenToMap(null,e,n)):(r.clusterHide(),r._recursivelyAddChildrenToMap(a,e,n)),i=h.length-1;i>=0;i--)o=h[i],n.contains(o._latlng)||s.removeLayer(o)}),this._forceLayout(),this._topClusterLevel._recursivelyBecomeVisible(n,e),s.eachLayer(function(t){t instanceof L.MarkerCluster||!t._icon||t.clusterShow()}),this._topClusterLevel._recursively(n,t,e,function(t){t._recursivelyRestoreChildPositions(e)}),this._enqueue(function(){this._topClusterLevel._recursively(n,t,0,function(t){s.removeLayer(t),t.clusterShow()}),this._animationEnd()})},_animationZoomOut:function(t,e){this._animationZoomOutSingle(this._topClusterLevel,t-1,e),this._topClusterLevel._recursivelyAddChildrenToMap(null,e,this._getExpandedVisibleBounds()),this._topClusterLevel._recursivelyRemoveChildrenFromMap(this._currentShownBounds,t,this._getExpandedVisibleBounds())},_animationAddLayer:function(t,e){var i=this,n=this._featureGroup;n.addLayer(t),e!==t&&(e._childCount>2?(e._updateIcon(),this._forceLayout(),this._animationStart(),t._setPos(this._map.latLngToLayerPoint(e.getLatLng())),t.clusterHide(),this._enqueue(function(){n.removeLayer(t),t.clusterShow(),i._animationEnd()})):(this._forceLayout(),i._animationStart(),i._animationZoomOutSingle(e,this._map.getMaxZoom(),this._map.getZoom())))}},_animationZoomOutSingle:function(t,e,i){var n=this._getExpandedVisibleBounds();t._recursivelyAnimateChildrenInAndAddSelfToMap(n,e+1,i);var s=this;this._forceLayout(),t._recursivelyBecomeVisible(n,i),this._enqueue(function(){if(1===t._childCount){var r=t._markers[0];r.setLatLng(r.getLatLng()),r.clusterShow&&r.clusterShow()}else t._recursively(n,i,0,function(t){t._recursivelyRemoveChildrenFromMap(n,e+1)});s._animationEnd()})},_animationEnd:function(){this._map&&(this._map._mapPane.className=this._map._mapPane.className.replace(" leaflet-cluster-anim","")),this._inZoomAnimation--,this.fire("animationend")},_forceLayout:function(){L.Util.falseFn(e.body.offsetWidth)}}),L.markerClusterGroup=function(t){return new L.MarkerClusterGroup(t)},L.MarkerCluster=L.Marker.extend({initialize:function(t,e,i,n){L.Marker.prototype.initialize.call(this,i?i._cLatLng||i.getLatLng():new L.LatLng(0,0),{icon:this}),this._group=t,this._zoom=e,this._markers=[],this._childClusters=[],this._childCount=0,this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._bounds=new L.LatLngBounds,i&&this._addChild(i),n&&this._addChild(n)},getAllChildMarkers:function(t){t=t||[];for(var e=this._childClusters.length-1;e>=0;e--)this._childClusters[e].getAllChildMarkers(t);for(var i=this._markers.length-1;i>=0;i--)t.push(this._markers[i]);return t},getChildCount:function(){return this._childCount},zoomToBounds:function(){for(var t,e=this._childClusters.slice(),i=this._group._map,n=i.getBoundsZoom(this._bounds),s=this._zoom+1,r=i.getZoom();e.length>0&&n>s;){s++;var o=[];for(t=0;ts?this._group._map.setView(this._latlng,s):r>=n?this._group._map.setView(this._latlng,r+1):this._group._map.fitBounds(this._bounds)},getBounds:function(){var t=new L.LatLngBounds;return t.extend(this._bounds),t},_updateIcon:function(){this._iconNeedsUpdate=!0,this._icon&&this.setIcon(this)},createIcon:function(){return this._iconNeedsUpdate&&(this._iconObj=this._group.options.iconCreateFunction(this),this._iconNeedsUpdate=!1),this._iconObj.createIcon()},createShadow:function(){return this._iconObj.createShadow()},_addChild:function(t,e){this._iconNeedsUpdate=!0,this._boundsNeedUpdate=!0,this._setClusterCenter(t),t instanceof L.MarkerCluster?(e||(this._childClusters.push(t),t.__parent=this),this._childCount+=t._childCount):(e||this._markers.push(t),this._childCount++),this.__parent&&this.__parent._addChild(t,!0)},_setClusterCenter:function(t){this._cLatLng||(this._cLatLng=t._cLatLng||t._latlng)},_resetBounds:function(){var t=this._bounds;t._southWest&&(t._southWest.lat=1/0,t._southWest.lng=1/0),t._northEast&&(t._northEast.lat=-1/0,t._northEast.lng=-1/0)},_recalculateBounds:function(){var t,e,i,n,s=this._markers,r=this._childClusters,o=0,a=0,h=this._childCount;if(0!==h){for(this._resetBounds(),t=0;t=0;i--)n=s[i],n._icon&&(n._setPos(e),n.clusterHide())},function(t){var i,n,s=t._childClusters;for(i=s.length-1;i>=0;i--)n=s[i],n._icon&&(n._setPos(e),n.clusterHide())})},_recursivelyAnimateChildrenInAndAddSelfToMap:function(t,e,i){this._recursively(t,i,0,function(n){n._recursivelyAnimateChildrenIn(t,n._group._map.latLngToLayerPoint(n.getLatLng()).round(),e),n._isSingleParent()&&e-1===i?(n.clusterShow(),n._recursivelyRemoveChildrenFromMap(t,e)):n.clusterHide(),n._addToMap()})},_recursivelyBecomeVisible:function(t,e){this._recursively(t,0,e,null,function(t){t.clusterShow()})},_recursivelyAddChildrenToMap:function(t,e,i){this._recursively(i,-1,e,function(n){if(e!==n._zoom)for(var s=n._markers.length-1;s>=0;s--){var r=n._markers[s];i.contains(r._latlng)&&(t&&(r._backupLatlng=r.getLatLng(),r.setLatLng(t),r.clusterHide&&r.clusterHide()),n._group._featureGroup.addLayer(r))}},function(e){e._addToMap(t)})},_recursivelyRestoreChildPositions:function(t){for(var e=this._markers.length-1;e>=0;e--){var i=this._markers[e];i._backupLatlng&&(i.setLatLng(i._backupLatlng),delete i._backupLatlng)}if(t-1===this._zoom)for(var n=this._childClusters.length-1;n>=0;n--)this._childClusters[n]._restorePosition();else for(var s=this._childClusters.length-1;s>=0;s--)this._childClusters[s]._recursivelyRestoreChildPositions(t)},_restorePosition:function(){this._backupLatlng&&(this.setLatLng(this._backupLatlng),delete this._backupLatlng)},_recursivelyRemoveChildrenFromMap:function(t,e,i){var n,s;this._recursively(t,-1,e-1,function(t){for(s=t._markers.length-1;s>=0;s--)n=t._markers[s],i&&i.contains(n._latlng)||(t._group._featureGroup.removeLayer(n),n.clusterShow&&n.clusterShow())},function(t){for(s=t._childClusters.length-1;s>=0;s--)n=t._childClusters[s],i&&i.contains(n._latlng)||(t._group._featureGroup.removeLayer(n),n.clusterShow&&n.clusterShow())})},_recursively:function(t,e,i,n,s){var r,o,a=this._childClusters,h=this._zoom;if(e>h)for(r=a.length-1;r>=0;r--)o=a[r],t.intersects(o._bounds)&&o._recursively(t,e,i,n,s);else if(n&&n(this),s&&this._zoom===i&&s(this),i>h)for(r=a.length-1;r>=0;r--)o=a[r],t.intersects(o._bounds)&&o._recursively(t,e,i,n,s)},_isSingleParent:function(){return this._childClusters.length>0&&this._childClusters[0]._childCount===this._childCount}}),L.Marker.include({clusterHide:function(){return this.options.opacityWhenUnclustered=this.options.opacity||1,this.setOpacity(0)},clusterShow:function(){var t=this.setOpacity(this.options.opacity||this.options.opacityWhenUnclustered);return delete this.options.opacityWhenUnclustered,t}}),L.DistanceGrid=function(t){this._cellSize=t,this._sqCellSize=t*t,this._grid={},this._objectPoint={}},L.DistanceGrid.prototype={addObject:function(t,e){var i=this._getCoord(e.x),n=this._getCoord(e.y),s=this._grid,r=s[n]=s[n]||{},o=r[i]=r[i]||[],a=L.Util.stamp(t);this._objectPoint[a]=e,o.push(t)},updateObject:function(t,e){this.removeObject(t),this.addObject(t,e)},removeObject:function(t,e){var i,n,s=this._getCoord(e.x),r=this._getCoord(e.y),o=this._grid,a=o[r]=o[r]||{},h=a[s]=a[s]||[];for(delete this._objectPoint[L.Util.stamp(t)],i=0,n=h.length;n>i;i++)if(h[i]===t)return h.splice(i,1),1===n&&delete a[s],!0},eachObject:function(t,e){var i,n,s,r,o,a,h,u=this._grid;for(i in u){o=u[i];for(n in o)for(a=o[n],s=0,r=a.length;r>s;s++)h=t.call(e,a[s]),h&&(s--,r--)}},getNearObject:function(t){var e,i,n,s,r,o,a,h,u=this._getCoord(t.x),_=this._getCoord(t.y),l=this._objectPoint,d=this._sqCellSize,c=null;for(e=_-1;_+1>=e;e++)if(s=this._grid[e])for(i=u-1;u+1>=i;i++)if(r=s[i])for(n=0,o=r.length;o>n;n++)a=r[n],h=this._sqDist(l[L.Util.stamp(a)],t),d>h&&(d=h,c=a);return c},_getCoord:function(t){return Math.floor(t/this._cellSize)},_sqDist:function(t,e){var i=e.x-t.x,n=e.y-t.y;return i*i+n*n}},function(){L.QuickHull={getDistant:function(t,e){var i=e[1].lat-e[0].lat,n=e[0].lng-e[1].lng;return n*(t.lat-e[0].lat)+i*(t.lng-e[0].lng)},findMostDistantPointFromBaseLine:function(t,e){var i,n,s,r=0,o=null,a=[];for(i=e.length-1;i>=0;i--)n=e[i],s=this.getDistant(n,t),s>0&&(a.push(n),s>r&&(r=s,o=n));return{maxPoint:o,newPoints:a}},buildConvexHull:function(t,e){var i=[],n=this.findMostDistantPointFromBaseLine(t,e);return n.maxPoint?(i=i.concat(this.buildConvexHull([t[0],n.maxPoint],n.newPoints)),i=i.concat(this.buildConvexHull([n.maxPoint,t[1]],n.newPoints))):[t[0]]},getConvexHull:function(t){var e,i=!1,n=!1,s=!1,r=!1,o=null,a=null,h=null,u=null,_=null,l=null;for(e=t.length-1;e>=0;e--){var d=t[e];(i===!1||d.lat>i)&&(o=d,i=d.lat),(n===!1||d.lats)&&(h=d,s=d.lng),(r===!1||d.lng=0;e--)t=i[e].getLatLng(),n.push(t);return L.QuickHull.getConvexHull(n)}}),L.MarkerCluster.include({_2PI:2*Math.PI,_circleFootSeparation:25,_circleStartAngle:Math.PI/6,_spiralFootSeparation:28,_spiralLengthStart:11,_spiralLengthFactor:5,_circleSpiralSwitchover:9,spiderfy:function(){if(this._group._spiderfied!==this&&!this._group._inZoomAnimation){var t,e=this.getAllChildMarkers(),i=this._group,n=i._map,s=n.latLngToLayerPoint(this._latlng);this._group._unspiderfy(),this._group._spiderfied=this,e.length>=this._circleSpiralSwitchover?t=this._generatePointsSpiral(e.length,s):(s.y+=10,t=this._generatePointsCircle(e.length,s)),this._animationSpiderfy(e,t)}},unspiderfy:function(t){this._group._inZoomAnimation||(this._animationUnspiderfy(t),this._group._spiderfied=null)},_generatePointsCircle:function(t,e){var i,n,s=this._group.options.spiderfyDistanceMultiplier*this._circleFootSeparation*(2+t),r=s/this._2PI,o=this._2PI/t,a=[];for(a.length=t,i=t-1;i>=0;i--)n=this._circleStartAngle+i*o,a[i]=new L.Point(e.x+r*Math.cos(n),e.y+r*Math.sin(n))._round();return a},_generatePointsSpiral:function(t,e){var i,n=this._group.options.spiderfyDistanceMultiplier,s=n*this._spiralLengthStart,r=n*this._spiralFootSeparation,o=n*this._spiralLengthFactor*this._2PI,a=0,h=[];for(h.length=t,i=t-1;i>=0;i--)a+=r/s+5e-4*i,h[i]=new L.Point(e.x+s*Math.cos(a),e.y+s*Math.sin(a))._round(),s+=o/a;return h},_noanimationUnspiderfy:function(){var t,e,i=this._group,n=i._map,s=i._featureGroup,r=this.getAllChildMarkers();for(this.setOpacity(1),e=r.length-1;e>=0;e--)t=r[e],s.removeLayer(t),t._preSpiderfyLatlng&&(t.setLatLng(t._preSpiderfyLatlng),delete t._preSpiderfyLatlng),t.setZIndexOffset&&t.setZIndexOffset(0),t._spiderLeg&&(n.removeLayer(t._spiderLeg),delete t._spiderLeg);i.fire("unspiderfied",{cluster:this,markers:r}),i._spiderfied=null}}),L.MarkerClusterNonAnimated=L.MarkerCluster.extend({_animationSpiderfy:function(t,e){var i,n,s,r,o=this._group,a=o._map,h=o._featureGroup,u=this._group.options.spiderLegPolylineOptions;for(i=0;i=0;n--)h=l.layerPointToLatLng(e[n]),s=t[n],s._preSpiderfyLatlng=s._latlng,s.setLatLng(h),s.clusterShow&&s.clusterShow(),f&&(r=s._spiderLeg,o=r._path,o.style.strokeDashoffset=0,r.setStyle({opacity:g}));this.setOpacity(.3),setTimeout(function(){_._animationEnd(),_.fire("spiderfied",{cluster:u,markers:t})},200)},_animationUnspiderfy:function(t){var e,i,n,s,r,o,a=this,h=this._group,u=h._map,_=h._featureGroup,l=t?u._latLngToNewLayerPoint(this._latlng,t.zoom,t.center):u.latLngToLayerPoint(this._latlng),d=this.getAllChildMarkers(),c=L.Path.SVG;for(h._animationStart(),this.setOpacity(1),i=d.length-1;i>=0;i--)e=d[i],e._preSpiderfyLatlng&&(e.setLatLng(e._preSpiderfyLatlng),delete e._preSpiderfyLatlng,o=!0,e._setPos&&(e._setPos(l),o=!1),e.clusterHide&&(e.clusterHide(),o=!1),o&&_.removeLayer(e),c&&(n=e._spiderLeg,s=n._path,r=s.getTotalLength()+.1,s.style.strokeDashoffset=r,n.setStyle({opacity:0})));setTimeout(function(){var t=0;for(i=d.length-1;i>=0;i--)e=d[i],e._spiderLeg&&t++;for(i=d.length-1;i>=0;i--)e=d[i],e._spiderLeg&&(e.clusterShow&&e.clusterShow(),e.setZIndexOffset&&e.setZIndexOffset(0),t>1&&_.removeLayer(e),u.removeLayer(e._spiderLeg),delete e._spiderLeg);h._animationEnd(),h.fire("unspiderfied",{cluster:a,markers:d})},200)}}),L.MarkerClusterGroup.include({_spiderfied:null,_spiderfierOnAdd:function(){this._map.on("click",this._unspiderfyWrapper,this),this._map.options.zoomAnimation&&this._map.on("zoomstart",this._unspiderfyZoomStart,this),this._map.on("zoomend",this._noanimationUnspiderfy,this)},_spiderfierOnRemove:function(){this._map.off("click",this._unspiderfyWrapper,this),this._map.off("zoomstart",this._unspiderfyZoomStart,this),this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._map.off("zoomend",this._noanimationUnspiderfy,this),this._noanimationUnspiderfy()},_unspiderfyZoomStart:function(){this._map&&this._map.on("zoomanim",this._unspiderfyZoomAnim,this)},_unspiderfyZoomAnim:function(t){L.DomUtil.hasClass(this._map._mapPane,"leaflet-touching")||(this._map.off("zoomanim",this._unspiderfyZoomAnim,this),this._unspiderfy(t))},_unspiderfyWrapper:function(){this._unspiderfy()},_unspiderfy:function(t){this._spiderfied&&this._spiderfied.unspiderfy(t)},_noanimationUnspiderfy:function(){this._spiderfied&&this._spiderfied._noanimationUnspiderfy()},_unspiderfyLayer:function(t){t._spiderLeg&&(this._featureGroup.removeLayer(t),t.clusterShow&&t.clusterShow(),t.setZIndexOffset&&t.setZIndexOffset(0),this._map.removeLayer(t._spiderLeg),delete t._spiderLeg)}}),L.MarkerClusterGroup.include({refreshClusters:function(t){return t?t instanceof L.MarkerClusterGroup?t=t._topClusterLevel.getAllChildMarkers():t instanceof L.LayerGroup?t=t._layers:t instanceof L.MarkerCluster?t=t.getAllChildMarkers():t instanceof L.Marker&&(t=[t]):t=this._topClusterLevel.getAllChildMarkers(),this._flagParentsIconsNeedUpdate(t),this._refreshClustersIcons(),this.options.singleMarkerMode&&this._refreshSingleMarkerModeMarkers(t),this},_flagParentsIconsNeedUpdate:function(t){var e,i;for(e in t)for(i=t[e].__parent;i;)i._iconNeedsUpdate=!0,i=i.__parent},_refreshClustersIcons:function(){this._featureGroup.eachLayer(function(t){t instanceof L.MarkerCluster&&t._iconNeedsUpdate&&t._updateIcon()})},_refreshSingleMarkerModeMarkers:function(t){var e,i;for(e in t)i=t[e],this.hasLayer(i)&&i.setIcon(this._overrideMarkerIcon(i))}}),L.Marker.include({refreshIconOptions:function(t,e){var i=this.options.icon;return L.setOptions(i,t),this.setIcon(i),e&&this.__parent&&this.__parent._group.refreshClusters(this),this}})}(window,document); \ No newline at end of file diff --git a/mapmakr/static/js/mapmakr.js b/mapmakr/static/js/mapmakr.js new file mode 100644 index 0000000..87ddfb4 --- /dev/null +++ b/mapmakr/static/js/mapmakr.js @@ -0,0 +1,172 @@ +/* */ + + +var map; +var drawnItems; +var statsControl; +var drawControl; + + +// http://stackoverflow.com/a/18324384/661150 +function callAjax(url, callback) +{ + var xmlhttp; + // compatible with IE7+, Firefox, Chrome, Opera, Safari + xmlhttp = new XMLHttpRequest(); + xmlhttp.onreadystatechange = function(){ + if (xmlhttp.readyState == 4 && xmlhttp.status == 200){ + callback(xmlhttp.responseText); + } + } + xmlhttp.open("GET", url, true); + xmlhttp.send(); +} + + +function gitmap_reload() { + callAjax("read.cgi", (function(req) { + var map = window.map; + drawnItems.clearLayers(); + + var objects = JSON.parse(req).objects; + var num_objects = objects.length; + for (var i=0; iroster
'; + text2 = ''; + popup = gitmap_file+' '+text1+' '+text2; + } + marker.options.id=gitmap_file; + marker.bindLabel(gitmap_file, { noHide: true }); + marker.bindPopup(popup); + drawnItems.addLayer(marker); + } + + if (drawControl == null) { + // add drawControl only after some drawnItems exist, so + // it is not shown disabled + drawControl = new L.Control.Draw({ + draw: { + position: 'topleft', + polygon: false, + rectangle: false, + polyline: false, + circle: false + }, + edit: { + featureGroup: drawnItems + } + }); + map.addControl(drawControl); + } + })); + callAjax("summary.cgi", function(req) { + var sc = window.statsControl; + var leaflet = 'Leaflet'; + sc.setPrefix (req + ' | ' + leaflet); + }); +} + + +function gitmap_setup() { + var osmUrl = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + osmAttrib = '© OpenStreetMap contributors', + osm = L.tileLayer(osmUrl, {maxZoom: 18, attribution: osmAttrib}); + + map = new L.Map('map', {layers: [osm], attributionControl: false, center: new L.LatLng(0, 0), zoom: 2 }); + + drawnItems = new L.MarkerClusterGroup({maxClusterRadius: 45}); // new L.FeatureGroup(); + map.addLayer(drawnItems); + gitmap_reload(); + + // drawcontrol formerly added here + + var refreshButton = new L.easyButton('icon ion-refresh', function() {gitmap_reload();}); + refreshButton.button.style.fontSize = '18px'; + map.addControl (refreshButton); + + var helpButton = new L.easyButton('icon ion-help-circled', function() {open("about.html");}); + helpButton.button.style.fontSize = '18px'; + map.addControl (helpButton); + + var controlSearch = new L.Control.Search({layer: drawnItems, + propertyName: 'id', + initial: false, + zoom: 12}); + map.addControl( controlSearch ); + + statsControl = new L.control.attribution({position:'bottomright', prefix:''}); + map.addControl(statsControl); + + scaleControl = new L.control.scale(); + map.addControl(scaleControl); + + terminator = new L.terminator(); + terminator.options.opacity=0.2; + terminator.options.fillOpacity=0.2; + terminator.addTo(map); + setInterval(function() {updateTerminator(terminator);}, 60000); // 1 minute + function updateTerminator(t) { + var t2 = L.terminator(); + t.setLatLngs(t2.getLatLngs()); + t.redraw(); + } + + // called after new marker is created + map.on('draw:created', function (e) { + var type = e.layerType, + layer = e.layer; + if (type !== 'marker') return; + + var userid = prompt("Marker name (e.g., kerberos userid):", ""); + if (userid != null) { + layer.options.id=userid; + layer.bindPopup(userid); + drawnItems.addLayer(layer); + callAjax("write.cgi?op=new"+ + "&id="+encodeURIComponent(userid)+ + "&location="+encodeURIComponent(JSON.stringify(layer.getLatLng()))+ + "&options="+encodeURIComponent("{}"), + function(req){alert("response:\n"+req); + gitmap_reload();}); + } + }); + + map.on('draw:edited', function (e) { + e.layers.eachLayer(function (layer) { + var type = layer.layerType; + var userid = layer.options.id; + callAjax("write.cgi?op=edit"+ + "&id="+encodeURIComponent(userid)+ + "&location="+encodeURIComponent(JSON.stringify(layer.getLatLng()))+ + "&options="+encodeURIComponent("{}"), + function(req){alert("response:\n"+req); + gitmap_reload();}); + }); + }); + + map.on('draw:deleted', function (e) { + e.layers.eachLayer(function (layer) { + var type = layer.layerType; + var userid = layer.options.id; + callAjax("write.cgi?op=delete"+ + "&id="+encodeURIComponent(userid), + function(req){alert("response:\n"+req); + gitmap_reload();}); + }); + }); + + + +} diff --git a/mapmakr/templates/about.html b/mapmakr/templates/about.html new file mode 100644 index 0000000..ed956be --- /dev/null +++ b/mapmakr/templates/about.html @@ -0,0 +1,100 @@ +about gitmap + + +

about gitmap

+ +

gitmap is a simple, small, 100% free/open-source, collaborative map +editor that runs mostly in a web browser. It integrates community +FOSS projects and data sources.

+ +run gitmap + +

FAQ

+ +

Why?

+

Because it's fun to browse around a map, and nice to know place +markers like in a wiki, and nice to find neighbours. This particular +installation is for locating Red Hat remotees / offices. Other installations +may store whatever they like.

+ +

Who?

+

fche (gitmap integration scripts), + Open Streetmap (background map), + Leaflet (map rendering javascript library + plugins), + and others.

+ +

How to use it?

+ +

Use the toolbar icons in top left. From top to bottom, these are:

+ +

Zoom/pan as per any other map type webapp.

+ +

To refresh your browser's copy of the markers, click the refresh icon.

+ +

To see this page, hit the question mark help icon.

+ +

To search for a marker by unique name substring, click the search +icon. Type a substring. Click on a hit shown in the list.

+ +

To add a marker, click the marker icon. Click again where the +marker should be put. A popup dialog box will ask for a unique +identifier. Please use your kerberos userid, to allow the roster/mojo +popup links to work. Use the +pattern office-AIRPORT-CITY-NAME for offices. Or pick whatever +will be useful to find your marker with the search function. + +

To move a marker, click the edit icon to enter +edit mode. Drag misplaced markers. Click on "save" right +beside the edit icon. Be rewarded by a popup containing git command +output from the back-end. Click on "cancel" if you changed your +mind.

+ +

To delete a marker, click the trash icon to enter delete +mode. (It might be greyed out; you can still click on it.) +Click on doomed markers. Click on "save" or "cancel".

+ +

How does it work?

+ +

Check it out.

+

The front-end consists of very small HTML + JavaScript files that +load Leaflet and OSM data from the Internet into your browser. The +front-end also loads marker data from our own servers via HTTP CGI. +Markers may be created, edited (moved), and deleted; these operations +also map to individual HTTP CGI calls to the server.

+

The back-end consists of bog-standard Apache HTTPD, a +plain-text-file "database" containing the collected markers, and a few +tiny CGI python scripts to relay the database to the web application. +(Calling it a database is an exaggeration - it is literally a +collection of text files, one per marker, whose contents are protected +by GIT.)

+ +

There is really not much to it. No GIS database, no server +processes, no fancy protocols. It took only about a day to get it +working, after spending a few days scouting out many +web-mapping-related software packages/services like postgis, cartodb, +opengeo, mapbox, openlayers, google, etc. In this case, FOSS does +the job, and easily.

+ +

Where to send patches?

+ +

Find gitmap sources +at https://gitlab.cee.redhat.com/fche/gitmap.git. +One or two third-party javascript libraries are stored there; others +are served from upstream projects' external CDNs. Feel free to fork / +experiment / deploy. Send patches to fche if desired.

+ +

Some hacking ideas: +

    +
  1. adding more marker metadata (full name, URLs)
  2. +
  3. adding a toggle switch to turn marker clustering on/off
  4. +
  5. taking a region-of-interest spec from the URL string (to make it easier to share maps already zoomed into a particular place)
  6. +
+Patches are welcome!

+ +

Please note that the version of the gitmap tree +in gitlab.cee contains snapshots of the real live +personal data RH folks have chosen to share. It is obviously +confidential.

+ + + diff --git a/mapmakr/templates/index.html b/mapmakr/templates/index.html new file mode 100644 index 0000000..f7e9f57 --- /dev/null +++ b/mapmakr/templates/index.html @@ -0,0 +1,30 @@ + + + + gitmap editor + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b700890 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,65 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "mapmakr" +description = 'An interactive map where you can place your own markers' +readme = "README.md" +requires-python = ">=3.7" +license = "MIT" +keywords = [] +authors = [ + { name = "Raoul Snyman", email = "raoul@snyman.info" }, +] +classifiers = [ + "Development Status :: 4 - Beta", + "Programming Language :: Python", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [ + "FastAPI", + "Jinja2", + "uvicorn[standard]" +] +dynamic = ["version"] + +[project.urls] +Documentation = "https://git.snyman.info/raoul/mapmakr#readme" +Issues = "https://git.snyman.info/raoul/mapmakr/issues" +Source = "https://git.snyman.info/raoul/mapmakr" + +[tool.hatch.version] +path = "mapmakr/__about__.py" + +[tool.hatch.envs.default] +dependencies = [ + "pytest", + "pytest-cov", +] +[tool.hatch.envs.default.scripts] +cov = "pytest --cov-report=term-missing --cov-config=pyproject.toml --cov=mapmakr --cov=tests {args}" +no-cov = "cov --no-cov {args}" + +[[tool.hatch.envs.test.matrix]] +python = ["37", "38", "39", "310", "311"] + +[tool.coverage.run] +branch = true +parallel = true +omit = [ + "mapmakr/__about__.py", +] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..5b501aa --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2023-present Raoul Snyman +# +# SPDX-License-Identifier: MIT