Initial commit

This commit is contained in:
Raoul Snyman 2023-04-11 13:51:59 -07:00
commit fa8146e8c4
22 changed files with 2175 additions and 0 deletions

9
LICENSE.txt Normal file
View File

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

21
README.md Normal file
View File

@ -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.

4
mapmakr/__about__.py Normal file
View File

@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2023-present Raoul Snyman <rsnyman@redhat.com>
#
# SPDX-License-Identifier: MIT
__version__ = '0.0.1'

3
mapmakr/__init__.py Normal file
View File

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2023-present Raoul Snyman <rsnyman@redhat.com>
#
# SPDX-License-Identifier: MIT

15
mapmakr/app.py Normal file
View File

@ -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)

143
mapmakr/static/3rdparty/L.Terminator.js vendored Normal file
View File

@ -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);
};

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 <a>
// }
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<this._states.length;i++){
this.button.appendChild(this._states[i].icon);
}
}
},
_currentState: {
// placeholder content
stateName: 'unnamed',
icon: (function(){ return document.createElement('span'); })()
},
_states: null, // populated on init
state: function(newState){
// activate by name
if(typeof newState == 'string'){
this._activateStateNamed(newState);
// activate by index
} else if (typeof newState == 'number'){
this._activateState(this._states[newState]);
}
return this;
},
_activateStateNamed: function(stateName){
for(var i = 0; i < this._states.length; i++){
if( this._states[i].stateName == stateName ){
this._activateState( this._states[i] );
}
}
},
_activateState: function(newState){
if( newState === this._currentState ){
// don't touch the dom if it'll just be the same after
return;
} else {
// swap out elements... if you're into that kind of thing
if( this.options.type == 'replace' ){
this.button.appendChild(newState.icon);
this.button.removeChild(this._currentState.icon);
}
if( newState.title ){
this.button.title = newState.title;
} else {
this.button.removeAttribute('title');
}
// update classes for animations
for(var i=0;i<this._states.length;i++){
L.DomUtil.removeClass(this._states[i].icon, this._currentState.stateName + '-active');
L.DomUtil.addClass(this._states[i].icon, newState.stateName + '-active');
}
// update classes for animations
L.DomUtil.removeClass(this.button, this._currentState.stateName + '-active');
L.DomUtil.addClass(this.button, newState.stateName + '-active');
// update the record
this._currentState = newState;
}
},
enable: function(){
L.DomUtil.addClass(this.button, 'enabled');
L.DomUtil.removeClass(this.button, 'disabled');
this.button.setAttribute('aria-hidden', 'false');
return this;
},
disable: function(){
L.DomUtil.addClass(this.button, 'disabled');
L.DomUtil.removeClass(this.button, 'enabled');
this.button.setAttribute('aria-hidden', 'true');
return this;
},
removeFrom: function (map) {
this._container.parentNode.removeChild(this._container);
this._map = null;
return this;
},
onAdd: function(){
var containerObj = L.easyBar([this], {
position: this.options.position,
leafletClasses: this.options.leafletClasses
});
this._container = containerObj.container;
return this._container;
}
});
L.easyButton = function(/* args will pass automatically */){
var args = Array.prototype.concat.apply([L.Control.EasyButton],arguments)
return new (Function.prototype.bind.apply(L.Control.EasyButton, args));
};
/*************************
*
* util functions
*
*************************/
// constructor for states so only curated
// states end up getting called
function State(template, easyButton){
this.title = template.title;
this.stateName = template.stateName ? template.stateName : 'unnamed-state';
// build the wrapper
this.icon = L.DomUtil.create('span', '');
L.DomUtil.addClass(this.icon, 'button-state state-' + this.stateName.replace(/(^\s*|\s*$)/g,''));
this.icon.innerHTML = buildIcon(template.icon);
this.onClick = L.Util.bind(template.onClick?template.onClick:function(){}, easyButton);
}
function buildIcon(ambiguousIconString) {
var tmpIcon;
// does this look like html? (i.e. not a class)
if( ambiguousIconString.match(/[&;=<>"']/) ){
// 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;
}
})();

View File

@ -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: "";
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -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;
}

View File

@ -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 = "<span>&otimes;</span>";//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<this._inputMinSize ? this._inputMinSize : this._input.value.length;
},
_handleArrowSelect: function(velocity) {
var searchTips = this._tooltip.hasChildNodes() ? this._tooltip.childNodes : [];
for (i=0; i<searchTips.length; i++)
L.DomUtil.removeClass(searchTips[i], 'search-tip-select');
if ((velocity == 1 ) && (this._tooltip.currentSelection >= (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<oldrad)
{
clearInterval(circle._timerAnimLoc);
circle.setRadius(oldrad);//reset radius
//if(typeof afterAnimCall == 'function')
//afterAnimCall();
//TODO use create event 'animateEnd' in L.Control.Search.Marker
}
}, tInt);
}
return this;
}
});
L.Map.addInitHook(function () {
if (this.options.searchControl) {
this.searchControl = L.control.search(this.options.searchControl);
this.addControl(this.searchControl);
}
});
L.control.search = function (options) {
return new L.Control.Search(options);
};
}).call(this);

File diff suppressed because one or more lines are too long

View File

@ -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; i<num_objects; i++) {
var gitmap_file = objects[i].file;
var gitmap_object = objects[i].contents;
var marker = L.marker(gitmap_object.location);
for (var opt in Object.keys(gitmap_object.options)) {
marker.options[opt] = gitmap_object.options[opt];
}
var popup = '';
if (gitmap_file.match(/^office/i)) {
marker.options.icon = L.AwesomeMarkers.icon({icon: 'building', prefix: 'fa', markerColor: 'red'});
popup = gitmap_file;
} else {
marker.options.icon = L.AwesomeMarkers.icon({icon: 'person', prefix: 'ion'});
text1 = '<a href="http://rover.redhat.com/people/profile/'+gitmap_file+'" target="_blank">roster</a>';
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 = '<a href="http://leafletjs.com">Leaflet</a>';
sc.setPrefix (req + ' | ' + leaflet);
});
}
function gitmap_setup() {
var osmUrl = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
osmAttrib = '&copy; <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> 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();});
});
});
}

View File

@ -0,0 +1,100 @@
<html><head><title>about gitmap</title></head>
<body>
<h1>about gitmap</h1>
<p>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.</p>
<b><a href="index.html">run gitmap</a></b>
<h1>FAQ</h1>
<h2>Why?</h2>
<p>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.</p>
<h2>Who?</h2>
<p><a href="mailto:fche@redhat.com">fche</a> (gitmap integration scripts),
<a href="http://www.openstreetmap.org">Open Streetmap</a> (background map),
<a href="http://www.leafletjs.com">Leaflet</a> (map rendering javascript library + plugins),
and others.</p>
<h2>How to use it?</h2>
<p>Use the toolbar icons in top left. From top to bottom, these are:</p>
<p>Zoom/pan as per any other map type webapp.</p>
<p>To refresh your browser's copy of the markers, click the refresh icon.</p>
<p>To see this page, hit the question mark help icon.</p>
<p>To search for a marker by unique name substring, click the search
icon. Type a substring. Click on a hit shown in the list.</p>
<p>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 <tt>office-AIRPORT-CITY-NAME</tt> for offices. Or pick whatever
will be useful to find your marker with the search function.
<p>To move a marker, click the edit icon to enter
<em>edit mode</em>. 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.</p>
<p>To delete a marker, click the trash icon to enter <em>delete
mode</em>. (It might be greyed out; you can still click on it.)
Click on doomed markers. Click on "save" or "cancel".</p>
<h2>How does it work?</h2>
<p><a href="https://gitlab.cee.redhat.com/fche/gitmap">Check it out.</a></p>
<p>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.</p>
<p>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.)</p>
<p>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.</p>
<h2>Where to send patches?</h2>
<p>Find gitmap sources
at <tt>https://gitlab.cee.redhat.com/fche/gitmap.git</tt>.
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 <tt>fche</tt> if desired.</p>
<p>Some hacking ideas:
<ol>
<li>adding more marker metadata (full name, URLs)</li>
<li>adding a toggle switch to turn marker clustering on/off</li>
<li>taking a region-of-interest spec from the URL string (to make it easier to share maps already zoomed into a particular place)</li>
</ol>
Patches are welcome!</p>
<p>Please note that the version of the gitmap tree
in <tt>gitlab.cee</tt> contains snapshots of the real live
personal data RH folks have chosen to share. It is obviously
confidential.</p>
</body>
</html>

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<head>
<title>gitmap editor</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.5/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/0.2.3/leaflet.draw.css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css"/>
<link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css" />
<link rel="stylesheet" href="3rdparty/leaflet-search.css" />
<link rel="stylesheet" href="3rdparty/leaflet-easybutton.css" />
<link rel="stylesheet" href="3rdparty/leaflet-label.css" />
<link rel="stylesheet" href="3rdparty/MarkerCluster.css"/>
<link rel="stylesheet" href="3rdparty/MarkerCluster.Default.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.5/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/0.2.3/leaflet.draw.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.min.js"></script>
<script src="3rdparty/leaflet-search.js"></script>
<script src="3rdparty/leaflet-easybutton.js"></script>
<script src="3rdparty/leaflet-label.js"></script>
<script src="3rdparty/L.Terminator.js"></script>
<script src="3rdparty/leaflet.markercluster.js"></script>
<script src="mapmakr.js"></script>
</head>
<body>
<div id="map" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%;"></div>
<script>setup_map();</script>
</body>
</html>

65
pyproject.toml Normal file
View File

@ -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:",
]

3
tests/__init__.py Normal file
View File

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2023-present Raoul Snyman <rsnyman@redhat.com>
#
# SPDX-License-Identifier: MIT