Привет. Была поставлена задача реализовать геолокацию (google maps v3) для пользователей в одном из проектов на django, хочу поделиться своим решением.
Как обычно разработка началась с поиска аналогичных решений, и за основу был взят пример дающий часть функционала, а именно 1 пункт. Ссылка на snippet. В нем был ряд недостатоков. Для добавления кастомных пользовательских полей в проекте я использовал стандартный модуль AUTH_PROFILE_MODULE. Соответственно в админке для редактирования профиля пользователя добавлял поля inline блоками (admin.StackedInline). При генерации разметки для этих inline блоков django использует в id для input'ов знаки "-". для добавления префиксов каждому блоку. Javascript, как известно, не любит в именах функций использование "-", поэтому первым делом все знаки "-" для имен функций преобразуем в "_".
Также сохранение координат было в виде строки «x,y» с последующим split'ом для вывода. Это вызвало бы конфликт при добавлении в поле еще и адреса в котором могли встречаться эти самые запятые. В качестве решения был использован еще один snippet дающий возможность использовать TextField для хранения JSON объектов. Ссылка на snippet. Таким образом было реализовано сохранение координат и адреса в виде JSON объекта:
Обработка при рендеринге на сервере:
Обработка на стороне клиента:
Добавлено поле для вывода текущей геолокации:
Для реализации 2 пункта был использован google.maps.Geocoder и jQuery autocomplete:
Добавлено поле для поиска по адресу:
В main.css лежит:
Вот так это выглядит в админке:
Всем спасибо!
Необходимый функционал:
- Вывод карты с маркером текущего положения, возможность перемещать маркер (dragged), ставить по click событию
- Поиск по адресу (autocomplete)
- Сохранение как координат, так и самого адреса (если он имеет место быть)
Как обычно разработка началась с поиска аналогичных решений, и за основу был взят пример дающий часть функционала, а именно 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;
}
Вот так это выглядит в админке:
Всем спасибо!