📜 ⬆️ ⬇️

Django Gmap v3 Widget - geolocation with search, saving coordinates and addresses in JSONField

Hey. The task was to implement geolocation (google maps v3) for users in one of the projects on django, I want to share my decision.

Required functionality:

  1. Output of the map with a marker of the current position, the ability to move the marker (dragged), put on a click event
  2. Search by address (autocomplete)
  3. Saving both the coordinates and the address itself (if it takes place)


As usual, the development began with the search for similar solutions, and the example was taken as the basis for giving the part of the functional, namely 1 point. Link to snippet . It had a number of flaws. To add custom custom fields in the project, I used the standard AUTH_PROFILE_MODULE module. Accordingly, in the admin panel for editing the user profile, added inline fields with blocks (admin.StackedInline). When generating markup for these inline blocks, django uses "-" signs for input id. to add prefixes to each block. Javascript, as is known, does not like the use of "-" in function names, so first of all all the signs "-" for function names will be converted to "_".
functionName=name.replace('-', '_') 

Also saving the coordinates was in the form of the string "x, y" followed by a split for output. This would cause a conflict when adding to the field also addresses in which these same commas could occur. As a solution, another snippet was used, which makes it possible to use TextField to store JSON objects. Link to snippet . Thus, the saving of coordinates and addresses in the form of a JSON object was implemented:
 value = {'lat': lat, 'lng': lng, 'address': address} 

Processing when rendering on the server:
 if value is None: lat, lng, address = DEFAULT_LAT, DEFAULT_LNG, DEFAULT_ADDRESS value = {'lat': lat, 'lng': lng, 'address': address} else: lat, lng, address = float(value['lat']), float(value['lng']), value['address'] curLocation = json.dumps(value, cls=DjangoJSONEncoder) 

Client side processing:
 function savePosition_%(functionName)s(point, address) { var input = document.getElementById("id_%(name)s"); var location = {'lat': point.lat().toFixed(6), 'lng': point.lng().toFixed(6)}; location.address = '%(defAddress)s'; if (address) { location.address = address; } input.value = JSON.stringify(location); map_%(functionName)s.panTo(point); } 

Added field to display current geolocation:
 html += '<br /><label>%s: </label><span>%s</span>' % (u' ', address) 

To implement 2 items, google.maps.Geocoder and jQuery autocomplete were used:
 google.maps.event.addListener(marker, 'dragend', function(mouseEvent) { geocoder.geocode({'latLng': mouseEvent.latLng}, function(results, status) { if (status == google.maps.GeocoderStatus.OK && results[0]) { $('#address_%(name)s').val(results[0].formatted_address); savePosition_%(functionName)s(mouseEvent.latLng, results[0].formatted_address); } else { savePosition_%(functionName)s(mouseEvent.latLng); } }); }); google.maps.event.addListener(map_%(functionName)s, 'click', function(mouseEvent){ marker.setPosition(mouseEvent.latLng); geocoder.geocode({'latLng': mouseEvent.latLng}, function(results, status) { if (status == google.maps.GeocoderStatus.OK && results[0]) { $('#address_%(name)s').val(results[0].formatted_address); savePosition_%(functionName)s(mouseEvent.latLng, results[0].formatted_address); } else { savePosition_%(functionName)s(mouseEvent.latLng); } }); }); $('#address_%(name)s').autocomplete({ source: function(request, response) { geocoder.geocode({'address': request.term}, function(results, status) { response($.map(results, function(item) { return { value: item.formatted_address, location: item.geometry.location } })); }) }, select: function(event, ui) { marker.setPosition(ui.item.location); savePosition_%(functionName)s(ui.item.location, ui.item.value); } }); 

Added field to search by address:
 html += '<label>%s: </label><input id="address_%s" type="text"/>' % (u'  ', name) 


Putting it all together turned out the following final snippet:

 from django.conf import settings from main.JSONField import JSONField from django.core.serializers.json import DjangoJSONEncoder from django.utils import simplejson as json DEFAULT_WIDTH = 300 DEFAULT_HEIGHT = 300 DEFAULT_LAT = 55.75 DEFAULT_LNG = 37.62 DEFAULT_ADDRESS = u'( )' class LocationWidget(forms.TextInput): def __init__(self, *args, **kw): self.map_width = kw.get("map_width", DEFAULT_WIDTH) self.map_height = kw.get("map_height", DEFAULT_HEIGHT) super(LocationWidget, self).__init__(*args, **kw) self.inner_widget = forms.widgets.HiddenInput() def render(self, name, value, *args, **kwargs): if value is None: lat, lng, address = DEFAULT_LAT, DEFAULT_LNG, DEFAULT_ADDRESS value = {'lat': lat, 'lng': lng, 'address': address} else: lat, lng, address = float(value['lat']), float(value['lng']), value['address'] curLocation = json.dumps(value, cls=DjangoJSONEncoder) js = ''' <script type="text/javascript"> //<![CDATA[ var map_%(functionName)s; function savePosition_%(functionName)s(point, address) { var input = document.getElementById("id_%(name)s"); var location = {'lat': point.lat().toFixed(6), 'lng': point.lng().toFixed(6)}; location.address = '%(defAddress)s'; if (address) { location.address = address; } input.value = JSON.stringify(location); map_%(functionName)s.panTo(point); } function load_%(functionName)s() { var point = new google.maps.LatLng(%(lat)f, %(lng)f); var options = { zoom: 13, center: point, mapTypeId: google.maps.MapTypeId.ROADMAP }; map_%(functionName)s = new google.maps.Map(document.getElementById("map_%(name)s"), options); geocoder = new google.maps.Geocoder(); var marker = new google.maps.Marker({ map: map_%(functionName)s, position: point, draggable: true }); google.maps.event.addListener(marker, 'dragend', function(mouseEvent) { geocoder.geocode({'latLng': mouseEvent.latLng}, function(results, status) { if (status == google.maps.GeocoderStatus.OK && results[0]) { $('#address_%(name)s').val(results[0].formatted_address); savePosition_%(functionName)s(mouseEvent.latLng, results[0].formatted_address); } else { savePosition_%(functionName)s(mouseEvent.latLng); } }); }); google.maps.event.addListener(map_%(functionName)s, 'click', function(mouseEvent){ marker.setPosition(mouseEvent.latLng); geocoder.geocode({'latLng': mouseEvent.latLng}, function(results, status) { if (status == google.maps.GeocoderStatus.OK && results[0]) { $('#address_%(name)s').val(results[0].formatted_address); savePosition_%(functionName)s(mouseEvent.latLng, results[0].formatted_address); } else { savePosition_%(functionName)s(mouseEvent.latLng); } }); }); $('#address_%(name)s').autocomplete({ source: function(request, response) { geocoder.geocode({'address': request.term}, function(results, status) { response($.map(results, function(item) { return { value: item.formatted_address, location: item.geometry.location } })); }) }, select: function(event, ui) { marker.setPosition(ui.item.location); savePosition_%(functionName)s(ui.item.location, ui.item.value); } }); } $(document).ready(function(){ load_%(functionName)s(); }); //]]> </script> ''' % dict(functionName=name.replace('-', '_'), name=name, lat=lat, lng=lng, defAddress=DEFAULT_ADDRESS) html = self.inner_widget.render("%s" % name, "%s" % curLocation, dict(id='id_%s' % name)) html += '<div id="map_%s" style="width: %dpx; height: %dpx"></div>' % (name, self.map_width, self.map_height) html += '<label>%s: </label><input id="address_%s" type="text"/>' % (u'  ', name) html += '<br /><label>%s: </label><span>%s</span>' % (u' ', address) return mark_safe(js + html) class Media: css = {'all': ( 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/themes/redmond/jquery-ui.css', settings.MEDIA_URL+'css/main.css', )} js = ( 'http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js', 'http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.18/jquery-ui.min.js', 'http://maps.google.com/maps/api/js?sensor=false', ) class LocationField(JSONField): def formfield(self, **kwargs): defaults = {'widget': LocationWidget} return super(LocationField, self).formfield(**defaults) 

')
PS

In main.css is:
 .ui-autocomplete li { list-style-type: none; } 

This is how it looks in the admin panel:


Thanks to all!

Source: https://habr.com/ru/post/141444/


All Articles