340 lines
8.8 KiB
JavaScript

/*
* Leaflet Location Picker v0.3.4 - 2022-11-18
*
* Copyright 2022 Stefano Cudini
* stefano.cudini@gmail.com
* https://opengeo.tech/
*
* Licensed under the MIT license.
*
* Demo:
* https://opengeo.tech/maps/leaflet-locationpicker/
*
* Source:
* git@github.com:stefanocudini/leaflet-locationpicker.git
*
*/
(function (factory) {
if(typeof define === 'function' && define.amd) {
//AMD
define(['jquery','leaflet'], factory);
} else if(typeof module !== 'undefined') {
// Node/CommonJS
module.exports = factory(require('jquery','leaflet'));
} else {
// Browser globals
if(typeof window.jQuery === 'undefined')
throw 'jQuery must be loaded first';
if(typeof window.L === 'undefined')
throw 'Leaflet must be loaded first';
factory(window.jQuery, window.L);
}
})(function(jQuery, L) {
var $ = jQuery;
$.fn.leafletLocationPicker = function(opts, onChangeLocation) {
var http = window.location.protocol;
var baseClassName = 'leaflet-locpicker',
baseLayers = {
'OSM': http + '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
// 'SAT': http + '//otile1.mqcdn.com/tiles/1.0.0/sat/{z}/{x}/{y}.png' // AK 2021-04-22: invalid URL!!
//TODO add more free base layers
};
var optsMap = {
zoom: 0,
center: L.latLng([40,0]),
zoomControl: false,
attributionControl: false
};
if($.isPlainObject(opts) && $.isPlainObject(opts.map))
optsMap = $.extend(optsMap, opts.map);
var defaults = {
alwaysOpen: false,
className: baseClassName,
location: optsMap.center,
locationFormat: '{lat}{sep}{lng}',
locationMarker: true,
locationDigits: 6,
locationSep: ',',
position: 'topright',
layer: 'OSM',
height: 140,
width: 200,
event: 'click',
cursorSize: '30px',
readOnly: false,
map: optsMap,
onChangeLocation: $.noop,
mapContainer: ""
};
if($.isPlainObject(opts))
opts = $.extend(defaults, opts);
else if($.isFunction(opts))
opts = $.extend(defaults, {
onChangeLocation: opts
});
else
opts = defaults;
if($.isFunction(onChangeLocation))
opts = $.extend(defaults, {
onChangeLocation: onChangeLocation
});
function roundLocation(loc) {
return loc ? L.latLng(
parseFloat(loc.lat).toFixed(opts.locationDigits),
parseFloat(loc.lng).toFixed(opts.locationDigits)
) : loc;
}
function parseLocation(loc) {
var retLoc = loc;
switch($.type(loc)) {
case 'string':
var ll = loc.split(opts.locationSep);
if(ll[0] && ll[1])
retLoc = L.latLng(ll);
else
retLoc = null;
break;
case 'array':
retLoc = L.latLng(loc);
break;
case 'object':
var lat, lng;
if(loc.hasOwnProperty('lat'))
lat = loc.lat;
else if(loc.hasOwnProperty('latitude'))
lat = loc.latitude;
if(loc.hasOwnProperty('lng'))
lng = loc.lng;
else if(loc.hasOwnProperty('lon'))
lng = loc.lon;
else if(loc.hasOwnProperty('longitude'))
lng = loc.longitude;
retLoc = L.latLng(parseFloat(lat),parseFloat(lng));
break;
default:
retLoc = loc;
}
return roundLocation( retLoc );
}
function buildMap(self) {
self.divMap = document.createElement('div');
self.$map = $(document.createElement('div'))
.addClass(opts.className + '-map')
.height(opts.height)
.width(opts.width)
.append(self.divMap);
if (opts.readOnly)
self.$map.addClass("read-only");
//adds either as global div or specified container
//if added to specified container add some style class
if(opts.mapContainer && $(opts.mapContainer))
self.$map.appendTo(opts.mapContainer)
.addClass('map-select');
else
self.$map.appendTo('body');
if(self.location)
opts.map.center = self.location;
if(typeof opts.layer === 'string' && baseLayers[opts.layer]) {
opts.map.layers = L.tileLayer(baseLayers[opts.layer]);
}else if (opts.layer instanceof L.TileLayer ||
opts.layer instanceof L.GridLayer ||
opts.layer instanceof L.LayerGroup) {
opts.map.layers = opts.layer;
}else {
opts.map.layers = L.tileLayer(baseLayers.OSM);
}
//leaflet map
self.map = L.map(self.divMap, opts.map)
.addControl( L.control.zoom({position: 'bottomright'}) )
.on(opts.event, function(e) {
if (!opts.readOnly)
self.setLocation(e.latlng);
});
if(opts.activeOnMove) {
self.map.on('move', function(e) {
self.setLocation(e.target.getCenter());
});
}
//only adds closeBtn if not alwaysOpen
if(opts.alwaysOpen!==true){
var xmap = L.control({position: 'topright'});
xmap.onAdd = function(map) {
var btn_holder = L.DomUtil.create('div', 'leaflet-bar');
var btn = L.DomUtil.create('a','leaflet-control '+opts.className+'-close');
btn.innerHTML = '×';
btn_holder.appendChild(btn);
L.DomEvent
.on(btn, 'click', L.DomEvent.stop, self)
.on(btn, 'click', self.closeMap, self);
return btn_holder;
};
xmap.addTo(self.map);
}
if(opts.locationMarker)
self.marker = buildMarker(self.location).addTo(self.map);
return self.$map;
}
function buildMarker(loc) {
return L.marker(parseLocation(loc) || L.latLng(0,0), {
icon: L.divIcon({
className: opts.className+'-marker',
iconAnchor: L.point(0, 0),
// TODO: get rid of inline CSS completely, in order to make it compliant with Content-Security-Policy that doesn't wallows 'unsafe-inline' CSS.
// AK: These additional styles can be set up with JavaScript, after creation of the marker icon element.
html: '<div' + ("30px" !== opts.cursorSize ? 'style="width: ' + opts.cursorSize + '; height: ' + opts.cursorSize + ';"' : '') + '>'+
'<div class="corner1"></div>'+
'<div class="corner2"></div>'+
'<div class="corner3"></div>'+
'<div class="corner4"></div>'+
'</div>'
})
});
}
$(this).each(function(index, input) {
var self = this;
self.$input = $(this);
self.options = opts; // access to options
self.setReadOnly = function(newReadOnly) {
opts.readOnly = newReadOnly;
self.$map.toggleClass("read-only", newReadOnly);
};
self.locationOri = self.$input.val();
self.onChangeLocation = function() {
var edata = {
latlng: self.location,
location: self.getLocation()
};
self.$input.trigger($.extend(edata, {
type: 'changeLocation'
}));
opts.onChangeLocation.call(self, edata);
};
self.setLocation = function(loc, noSet) {
loc = loc || defaults.location;
self.location = parseLocation(loc);
if(self.marker)
self.marker.setLatLng(loc);
if(!noSet) {
self.$input.data('location', self.location);
self.$input.val( self.getLocation() );
self.onChangeLocation();
}
};
self.getLocation = function() {
return self.location ? L.Util.template(opts.locationFormat, {
lat: self.location.lat,
lng: self.location.lng,
sep: opts.locationSep
}) : self.location;
};
self.updatePosition = function() {
switch(opts.position) {
case 'bottomleft':
self.$map.css({
top: self.$input.offset().top + self.$input.height() + 6,
left: self.$input.offset().left
});
break;
case 'topright':
self.$map.css({
top: self.$input.offset().top,
left: self.$input.offset().left + self.$input.width() + 5
});
break;
}
};
self.openMap = function() {
self.updatePosition();
self.$map.show();
self.map.invalidateSize();
self.$input.trigger('show');
};
self.closeMap = function() {
self.$map.hide();
self.$input.trigger('hide');
};
self.setLocation(self.locationOri, true);
self.$map = buildMap(self);
self.$input
.addClass(opts.className)
.on('focus.'+opts.className, function(e) {
e.preventDefault();
self.openMap();
})
.on('blur.'+opts.className, function(e) {
e.preventDefault();
var p = e.relatedTarget;
var close = true;
while (p) {
if (p._leaflet) {
close = false;
break;
}
p = p.parentElement;
}
if(close) {
setTimeout(function() {
self.closeMap();
}, 100);
}
});
$(window).on('resize', function() {
if (self.$map.is(':visible'))
self.updatePosition();
});
//opens map initially if alwaysOpen
if(opts.alwaysOpen && opts.alwaysOpen===true) self.openMap();
});
return this;
};
});