diff --git a/src/agenda_culturel/forms.py b/src/agenda_culturel/forms.py index e121ef9..697e192 100644 --- a/src/agenda_culturel/forms.py +++ b/src/agenda_culturel/forms.py @@ -742,15 +742,23 @@ class PlaceForm(ModelForm): fields = "__all__" widgets = {"location": TextInput()} - def __init__(self, *args, **kwargs): - self.force_adjust = kwargs.pop("force_adjust", None) - super().__init__(*args, **kwargs) - def as_grid(self): result = ('
' + super().as_p() - + '
' - + '

Cliquez pour ajuster la position GPS

') + + '''
+
+

Cliquez pour ajuster la position GPS

+ Verrouiller la position + +
''') return mark_safe(result) diff --git a/src/agenda_culturel/static/location_field/js/form.js b/src/agenda_culturel/static/location_field/js/form.js new file mode 100644 index 0000000..85f9cc3 --- /dev/null +++ b/src/agenda_culturel/static/location_field/js/form.js @@ -0,0 +1,673 @@ +var SequentialLoader = function() { + var SL = { + loadJS: function(src, onload) { + //console.log(src); + // add to pending list + this._load_pending.push({'src': src, 'onload': onload}); + // check if not already loading + if ( ! this._loading) { + this._loading = true; + // load first + this.loadNextJS(); + } + }, + + loadNextJS: function() { + // get next + var next = this._load_pending.shift(); + if (next == undefined) { + // nothing to load + this._loading = false; + return; + } + // check not loaded + if (this._load_cache[next.src] != undefined) { + next.onload(); + this.loadNextJS(); + return; // already loaded + } + else { + this._load_cache[next.src] = 1; + } + // load + var el = document.createElement('script'); + el.type = 'application/javascript'; + el.src = next.src; + // onload callback + var self = this; + el.onload = function(){ + //console.log('Loaded: ' + next.src); + // trigger onload + next.onload(); + // try to load next + self.loadNextJS(); + }; + document.body.appendChild(el); + }, + + _loading: false, + _load_pending: [], + _load_cache: {} + }; + + return { + loadJS: SL.loadJS.bind(SL) + } +}; + + +!function($){ + var LocationFieldCache = { + load: [], + onload: {}, + + isLoading: false + }; + + var LocationFieldResourceLoader; + + $.locationField = function(options) { + var LocationField = { + options: $.extend({ + provider: 'google', + providerOptions: { + google: { + api: '//maps.google.com/maps/api/js', + mapType: 'ROADMAP' + } + }, + searchProvider: 'google', + id: 'map', + latLng: '0,0', + mapOptions: { + zoom: 9 + }, + basedFields: $(), + inputField: $(), + suffix: '', + path: '', + fixMarker: true + }, options), + + providers: /google|openstreetmap|mapbox/, + searchProviders: /google|yandex|nominatim|addok/, + + render: function() { + this.$id = $('#' + this.options.id); + + if ( ! this.providers.test(this.options.provider)) { + this.error('render failed, invalid map provider: ' + this.options.provider); + return; + } + + if ( ! this.searchProviders.test(this.options.searchProvider)) { + this.error('render failed, invalid search provider: ' + this.options.searchProvider); + return; + } + + var self = this; + + this.loadAll(function(){ + var mapOptions = self._getMapOptions(), + map = self._getMap(mapOptions); + + var marker = self._getMarker(map, mapOptions.center); + + // fix issue w/ marker not appearing + if (self.options.provider == 'google' && self.options.fixMarker) + self.__fixMarker(); + + // watch based fields + self._watchBasedFields(map, marker); + }); + }, + + fill: function(latLng) { + this.options.inputField.val(latLng.lat + ',' + latLng.lng); + }, + + search: function(map, marker, address) { + if (this.options.searchProvider === 'google') { + var provider = new GeoSearch.GoogleProvider({ apiKey: this.options.providerOptions.google.apiKey }); + provider.search({query: address}).then(data => { + if (data.length > 0) { + var result = data[0], + latLng = new L.LatLng(result.y, result.x); + + marker.setLatLng(latLng); + map.panTo(latLng); + } + }); + } + + else if (this.options.searchProvider === 'yandex') { + // https://yandex.com/dev/maps/geocoder/doc/desc/concepts/input_params.html + var url = 'https://geocode-maps.yandex.ru/1.x/?format=json&geocode=' + address; + + if (typeof this.options.providerOptions.yandex.apiKey !== 'undefined') { + url += '&apikey=' + this.options.providerOptions.yandex.apiKey; + } + + var request = new XMLHttpRequest(); + request.open('GET', url, true); + + request.onload = function () { + if (request.status >= 200 && request.status < 400) { + var data = JSON.parse(request.responseText); + var pos = data.response.GeoObjectCollection.featureMember[0].GeoObject.Point.pos.split(' '); + var latLng = new L.LatLng(pos[1], pos[0]); + marker.setLatLng(latLng); + map.panTo(latLng); + } else { + console.error('Yandex geocoder error response'); + } + }; + + request.onerror = function () { + console.error('Check connection to Yandex geocoder'); + }; + + request.send(); + } + + else if (this.options.searchProvider === 'addok') { + var url = 'https://api-adresse.data.gouv.fr/search/?limit=1&q=' + address; + + var request = new XMLHttpRequest(); + request.open('GET', url, true); + + request.onload = function () { + if (request.status >= 200 && request.status < 400) { + var data = JSON.parse(request.responseText); + var pos = data.features[0].geometry.coordinates; + var latLng = new L.LatLng(pos[1], pos[0]); + marker.setLatLng(latLng); + map.panTo(latLng); + } else { + console.error('Addok geocoder error response'); + } + }; + + request.onerror = function () { + console.error('Check connection to Addok geocoder'); + }; + + request.send(); + } + + else if (this.options.searchProvider === 'nominatim') { + var url = '//nominatim.openstreetmap.org/search?format=json&q=' + address; + + var request = new XMLHttpRequest(); + request.open('GET', url, true); + + request.onload = function () { + if (request.status >= 200 && request.status < 400) { + var data = JSON.parse(request.responseText); + if (data.length > 0) { + var pos = data[0]; + var latLng = new L.LatLng(pos.lat, pos.lon); + marker.setLatLng(latLng); + map.panTo(latLng); + } else { + console.error(address + ': not found via Nominatim'); + } + } else { + console.error('Nominatim geocoder error response'); + } + }; + + request.onerror = function () { + console.error('Check connection to Nominatim geocoder'); + }; + + request.send(); + } + }, + + loadAll: function(onload) { + this.$id.html('Loading...'); + + // resource loader + if (LocationFieldResourceLoader == undefined) + LocationFieldResourceLoader = SequentialLoader(); + + this.load.loader = LocationFieldResourceLoader; + this.load.path = this.options.path; + + var self = this; + + this.load.common(function(){ + var mapProvider = self.options.provider, + onLoadMapProvider = function() { + var searchProvider = self.options.searchProvider + 'SearchProvider', + onLoadSearchProvider = function() { + self.$id.html(''); + onload(); + }; + + if (self.load[searchProvider] != undefined) { + self.load[searchProvider](self.options.providerOptions[self.options.searchProvider] || {}, onLoadSearchProvider); + } + else { + onLoadSearchProvider(); + } + }; + + if (self.load[mapProvider] != undefined) { + self.load[mapProvider](self.options.providerOptions[mapProvider] || {}, onLoadMapProvider); + } + else { + onLoadMapProvider(); + } + }); + }, + + load: { + google: function(options, onload) { + var js = [ + this.path + '/@googlemaps/js-api-loader/index.min.js', + this.path + '/Leaflet.GoogleMutant.js', + ]; + + this._loadJSList(js, function(){ + const loader = new google.maps.plugins.loader.Loader({ + apiKey: options.apiKey, + version: "weekly", + }); + loader.load().then(() => onload()); + }); + }, + + googleSearchProvider: function(options, onload) { + onload(); + //var url = options.api; + + //if (typeof options.apiKey !== 'undefined') { + // url += url.indexOf('?') === -1 ? '?' : '&'; + // url += 'key=' + options.apiKey; + //} + + //var js = [ + // url, + // this.path + '/l.geosearch.provider.google.js' + // ]; + + //this._loadJSList(js, function(){ + // // https://github.com/smeijer/L.GeoSearch/issues/57#issuecomment-148393974 + // L.GeoSearch.Provider.Google.Geocoder = new google.maps.Geocoder(); + + // onload(); + //}); + }, + + yandexSearchProvider: function (options, onload) { + onload(); + }, + + mapbox: function(options, onload) { + onload(); + }, + + openstreetmap: function(options, onload) { + onload(); + }, + + common: function(onload) { + var self = this, + js = [ + // map providers + this.path + '/leaflet/leaflet.js', + // search providers + this.path + '/leaflet-geosearch/geosearch.umd.js', + ], + css = [ + // map providers + this.path + '/leaflet/leaflet.css' + ]; + + // Leaflet docs note: + // Include Leaflet JavaScript file *after* Leaflet’s CSS + // https://leafletjs.com/examples/quick-start/ + this._loadCSSList(css, function(){ + self._loadJSList(js, onload); + }); + }, + + _loadJS: function(src, onload) { + this.loader.loadJS(src, onload); + }, + + _loadJSList: function(srclist, onload) { + this.__loadList(this._loadJS, srclist, onload); + }, + + _loadCSS: function(src, onload) { + if (LocationFieldCache.onload[src] != undefined) { + onload(); + } + else { + LocationFieldCache.onload[src] = 1; + onloadCSS(loadCSS(src), onload); + } + }, + + _loadCSSList: function(srclist, onload) { + this.__loadList(this._loadCSS, srclist, onload); + }, + + __loadList: function(fn, srclist, onload) { + if (srclist.length > 1) { + for (var i = 0; i < srclist.length-1; ++i) { + fn.call(this, srclist[i], function(){}); + } + } + + fn.call(this, srclist[srclist.length-1], onload); + } + }, + + error: function(message) { + console.log(message); + this.$id.html(message); + }, + + _getMap: function(mapOptions) { + var map = new L.Map(this.options.id, mapOptions), layer; + + if (this.options.provider == 'google') { + layer = new L.gridLayer.googleMutant({ + type: this.options.providerOptions.google.mapType.toLowerCase(), + }); + } + else if (this.options.provider == 'openstreetmap') { + layer = new L.tileLayer( + '//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 18 + }); + } + else if (this.options.provider == 'mapbox') { + layer = new L.tileLayer( + 'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', { + maxZoom: 18, + accessToken: this.options.providerOptions.mapbox.access_token, + id: 'mapbox/streets-v11' + }); + } + + map.addLayer(layer); + + return map; + }, + + _getMapOptions: function() { + return $.extend(this.options.mapOptions, { + center: this._getLatLng() + }); + }, + + _getLatLng: function() { + var l = this.options.latLng.split(',').map(parseFloat); + return new L.LatLng(l[0], l[1]); + }, + + _getMarker: function(map, center) { + var self = this, + markerOptions = { + draggable: true + }; + + var marker = L.marker(center, markerOptions).addTo(map); + + marker.on('dragstart', function(){ + if (self.options.inputField.is('[readonly]')) + marker.dragging.disable(); + else + marker.dragging.enable(); + }); + + // fill input on dragend + marker.on('dragend move', function(){ + if (!self.options.inputField.is('[readonly]')) + self.fill(this.getLatLng()); + }); + + // place marker on map click + map.on('click', function(e){ + if (!self.options.inputField.is('[readonly]')) { + marker.setLatLng(e.latlng); + marker.dragging.enable(); + } + }); + + return marker; + }, + + _watchBasedFields: function(map, marker) { + var self = this, + basedFields = this.options.basedFields, + onchangeTimer, + onchange = function() { + if (!self.options.inputField.is('[readonly]')) { + var values = basedFields.map(function() { + var value = $(this).val(); + return value === '' ? null : value; + }); + var address = values.toArray().join(', '); + clearTimeout(onchangeTimer); + onchangeTimer = setTimeout(function(){ + self.search(map, marker, address); + }, 300); + } + }; + + basedFields.each(function(){ + var el = $(this); + + if (el.is('select')) + el.change(onchange); + else + el.keyup(onchange); + }); + + if (this.options.inputField.val() === '') { + var values = basedFields.map(function() { + var value = $(this).val(); + return value === '' ? null : value; + }); + var address = values.toArray().join(', '); + if (address !== '') + onchange(); + } + }, + + __fixMarker: function() { + $('.leaflet-map-pane').css('z-index', '2 !important'); + $('.leaflet-google-layer').css('z-index', '1 !important'); + } + } + + return { + render: LocationField.render.bind(LocationField) + } + } + + function dataLocationFieldObserver(callback) { + function _findAndEnableDataLocationFields() { + var dataLocationFields = $('input[data-location-field-options]'); + + dataLocationFields + .filter(':not([data-location-field-observed])') + .attr('data-location-field-observed', true) + .each(callback); + } + + var observer = new MutationObserver(function(mutations){ + _findAndEnableDataLocationFields(); + }); + + var container = document.documentElement || document.body; + + $(container).ready(function(){ + _findAndEnableDataLocationFields(); + }); + + observer.observe(container, {attributes: true}); + } + + dataLocationFieldObserver(function(){ + var el = $(this); + + var name = el.attr('name'), + options = el.data('location-field-options'), + basedFields = options.field_options.based_fields, + pluginOptions = { + id: 'map_' + name, + inputField: el, + latLng: el.val() || '0,0', + suffix: options['search.suffix'], + path: options['resources.root_path'], + provider: options['map.provider'], + searchProvider: options['search.provider'], + providerOptions: { + google: { + api: options['provider.google.api'], + apiKey: options['provider.google.api_key'], + mapType: options['provider.google.map_type'] + }, + mapbox: { + access_token: options['provider.mapbox.access_token'] + }, + yandex: { + apiKey: options['provider.yandex.api_key'] + }, + }, + mapOptions: { + zoom: options['map.zoom'] + } + }; + + // prefix + var prefixNumber; + + try { + prefixNumber = name.match(/-(\d+)-/)[1]; + } catch (e) {} + + if (options.field_options.prefix) { + var prefix = options.field_options.prefix; + + if (prefixNumber != null) { + prefix = prefix.replace(/__prefix__/, prefixNumber); + } + + basedFields = basedFields.map(function(n){ + return prefix + n + }); + } + + // based fields + pluginOptions.basedFields = $(basedFields.map(function(n){ + return '#id_' + n + }).join(',')); + + // render + $.locationField(pluginOptions).render(); + }); + +}(jQuery || django.jQuery); + +/*! +loadCSS: load a CSS file asynchronously. +[c]2015 @scottjehl, Filament Group, Inc. +Licensed MIT +*/ +(function(w){ + "use strict"; + /* exported loadCSS */ + var loadCSS = function( href, before, media ){ + // Arguments explained: + // `href` [REQUIRED] is the URL for your CSS file. + // `before` [OPTIONAL] is the element the script should use as a reference for injecting our stylesheet before + // By default, loadCSS attempts to inject the link after the last stylesheet or script in the DOM. However, you might desire a more specific location in your document. + // `media` [OPTIONAL] is the media type or query of the stylesheet. By default it will be 'all' + var doc = w.document; + var ss = doc.createElement( "link" ); + var ref; + if( before ){ + ref = before; + } + else { + var refs = ( doc.body || doc.getElementsByTagName( "head" )[ 0 ] ).childNodes; + ref = refs[ refs.length - 1]; + } + + var sheets = doc.styleSheets; + ss.rel = "stylesheet"; + ss.href = href; + // temporarily set media to something inapplicable to ensure it'll fetch without blocking render + ss.media = "only x"; + + // Inject link + // Note: the ternary preserves the existing behavior of "before" argument, but we could choose to change the argument to "after" in a later release and standardize on ref.nextSibling for all refs + // Note: `insertBefore` is used instead of `appendChild`, for safety re: http://www.paulirish.com/2011/surefire-dom-element-insertion/ + ref.parentNode.insertBefore( ss, ( before ? ref : ref.nextSibling ) ); + // A method (exposed on return object for external use) that mimics onload by polling until document.styleSheets until it includes the new sheet. + var onloadcssdefined = function( cb ){ + var resolvedHref = ss.href; + var i = sheets.length; + while( i-- ){ + if( sheets[ i ].href === resolvedHref ){ + return cb(); + } + } + setTimeout(function() { + onloadcssdefined( cb ); + }); + }; + + // once loaded, set link's media back to `all` so that the stylesheet applies once it loads + ss.onloadcssdefined = onloadcssdefined; + onloadcssdefined(function() { + ss.media = media || "all"; + }); + return ss; + }; + // commonjs + if( typeof module !== "undefined" ){ + module.exports = loadCSS; + } + else { + w.loadCSS = loadCSS; + } +}( typeof global !== "undefined" ? global : this )); + + +/*! +onloadCSS: adds onload support for asynchronous stylesheets loaded with loadCSS. +[c]2014 @zachleat, Filament Group, Inc. +Licensed MIT +*/ + +/* global navigator */ +/* exported onloadCSS */ +function onloadCSS( ss, callback ) { + ss.onload = function() { + ss.onload = null; + if( callback ) { + callback.call( ss ); + } + }; + + // This code is for browsers that don’t support onload, any browser that + // supports onload should use that instead. + // No support for onload: + // * Android 4.3 (Samsung Galaxy S4, Browserstack) + // * Android 4.2 Browser (Samsung Galaxy SIII Mini GT-I8200L) + // * Android 2.3 (Pantech Burst P9070) + + // Weak inference targets Android < 4.4 + if( "isApplicationInstalled" in navigator && "onloadcssdefined" in ss ) { + ss.onloadcssdefined( callback ); + } +} diff --git a/src/agenda_culturel/views.py b/src/agenda_culturel/views.py index 46b7f77..eb7aa39 100644 --- a/src/agenda_culturel/views.py +++ b/src/agenda_culturel/views.py @@ -1871,13 +1871,6 @@ class PlaceFromEventCreateView(PlaceCreateView): context["event"] = self.event return context - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - if self.event.location and "add" in self.request.GET: - kwargs["force_adjust"] = True - return kwargs - - def get_initial(self, *args, **kwargs): initial = super().get_initial(**kwargs) self.event = get_object_or_404(Event, pk=self.kwargs["pk"]) @@ -1888,6 +1881,7 @@ class PlaceFromEventCreateView(PlaceCreateView): initial["name"] = name initial["address"] = address initial["city"] = city + initial["location"] = "" return initial