Django Gmap v3 Widget — геолокация с поиском, сохранение координат и адреса в JSONField

в 7:04, , рубрики: django, python, метки: ,

Привет. Была поставлена задача реализовать геолокацию (google maps v3) для пользователей в одном из проектов на django, хочу поделиться своим решением.

Необходимый функционал:

  1. Вывод карты с маркером текущего положения, возможность перемещать маркер (dragged), ставить по click событию
  2. Поиск по адресу (autocomplete)
  3. Сохранение как координат, так и самого адреса (если он имеет место быть)


Как обычно разработка началась с поиска аналогичных решений, и за основу был взят пример дающий часть функционала, а именно 1 пункт. Ссылка на snippet. В нем был ряд недостатоков. Для добавления кастомных пользовательских полей в проекте я использовал стандартный модуль AUTH_PROFILE_MODULE. Соответственно в админке для редактирования профиля пользователя добавлял поля inline блоками (admin.StackedInline). При генерации разметки для этих inline блоков django использует в id для input'ов знаки "-". для добавления префиксов каждому блоку. Javascript, как известно, не любит в именах функций использование "-", поэтому первым делом все знаки "-" для имен функций преобразуем в "_".

functionName=name.replace('-', '_')

Также сохранение координат было в виде строки «x,y» с последующим split'ом для вывода. Это вызвало бы конфликт при добавлении в поле еще и адреса в котором могли встречаться эти самые запятые. В качестве решения был использован еще один snippet дающий возможность использовать TextField для хранения JSON объектов. Ссылка на snippet. Таким образом было реализовано сохранение координат и адреса в виде JSON объекта:

value = {'lat': lat, 'lng': lng, 'address': address}

Обработка при рендериге на сервере:

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)

Обработка на стороне клиента:

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

Добавлено поле для вывода текущей геолокации:

html += '<br /><label>%s: </label><span>%s</span>' % (u'Текущий адрес', address)

Для реализации 2 пункта был использован google.maps.Geocoder и jQuery autocomplete:

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

Добавлено поле для поиска по адресу:

html += '<label>%s: </label><input id="address_%s" type="text"/>' % (u'Поиск по адресу', name)
Собрав все воедино получился следующий финальный 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)
P.S.

В main.css лежит:

.ui-autocomplete li {
    list-style-type: none;
}

Вот так это выглядит в админке:
Django Gmap v3 Widget — геолокация с поиском, сохранение координат и адреса в JSONField

Всем спасибо!

Автор: dubenko

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js