Fix #1416 -- Add canonical geodata field (#1458)

* Fix #1416 -- Add canonical geodata field for events and subevents

* Add optional geocoding through OpenCageData

* Fix markup everywhere

* Add Leaflet map to geo coordinates

* Fix tests, add credits

* Fix spelling
This commit is contained in:
Raphael Michel
2019-10-21 13:07:35 +02:00
committed by GitHub
parent 19b10e3ca4
commit 27538d220e
30 changed files with 1098 additions and 9 deletions

View File

@@ -70,7 +70,7 @@ class EventSerializer(I18nAwareModelSerializer):
model = Event
fields = ('name', 'slug', 'live', 'testmode', 'currency', 'date_from',
'date_to', 'date_admission', 'is_public', 'presale_start',
'presale_end', 'location', 'has_subevents', 'meta_data', 'seating_plan',
'presale_end', 'location', 'geo_lat', 'geo_lon', 'has_subevents', 'meta_data', 'seating_plan',
'plugins', 'seat_category_mapping')
def validate(self, data):
@@ -277,8 +277,9 @@ class SubEventSerializer(I18nAwareModelSerializer):
class Meta:
model = SubEvent
fields = ('id', 'name', 'date_from', 'date_to', 'active', 'date_admission',
'presale_start', 'presale_end', 'location', 'event', 'is_public', 'seating_plan',
'item_price_overrides', 'variation_price_overrides', 'meta_data', 'seat_category_mapping')
'presale_start', 'presale_end', 'location', 'geo_lat', 'geo_lon', 'event', 'is_public',
'seating_plan', 'item_price_overrides', 'variation_price_overrides', 'meta_data',
'seat_category_mapping')
def validate(self, data):
data = super().validate(data)

View File

@@ -14,6 +14,7 @@ from django.utils.translation.trans_real import (
parse_accept_lang_header,
)
from pretix.base.settings import GlobalSettingsObject
from pretix.multidomain.urlreverse import get_domain
_supported = None
@@ -187,6 +188,11 @@ class SecurityMiddleware(MiddlewareMixin):
# https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
img_src = []
gs = GlobalSettingsObject()
if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
h = {
'default-src': ["{static}"],
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
@@ -196,7 +202,7 @@ class SecurityMiddleware(MiddlewareMixin):
'child-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}", "https://checkout.stripe.com"],
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"],
'img-src': ["{static}", "{media}", "data:", "https://*.stripe.com"] + img_src,
'font-src': ["{static}"],
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs

View File

@@ -0,0 +1,33 @@
# Generated by Django 2.2.1 on 2019-10-19 13:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pretixbase', '0138_auto_20191017_1151'),
]
operations = [
migrations.AddField(
model_name='event',
name='geo_lat',
field=models.FloatField(null=True),
),
migrations.AddField(
model_name='event',
name='geo_lon',
field=models.FloatField(null=True),
),
migrations.AddField(
model_name='subevent',
name='geo_lat',
field=models.FloatField(null=True),
),
migrations.AddField(
model_name='subevent',
name='geo_lon',
field=models.FloatField(null=True),
),
]

View File

@@ -324,6 +324,14 @@ class Event(EventMixin, LoggedModel):
max_length=200,
verbose_name=_("Location"),
)
geo_lat = models.FloatField(
verbose_name=_("Latitude"),
null=True, blank=True,
)
geo_lon = models.FloatField(
verbose_name=_("Longitude"),
null=True, blank=True,
)
plugins = models.TextField(
null=False, blank=True,
verbose_name=_("Plugins"),
@@ -929,6 +937,14 @@ class SubEvent(EventMixin, LoggedModel):
max_length=200,
verbose_name=_("Location"),
)
geo_lat = models.FloatField(
verbose_name=_("Latitude"),
null=True, blank=True,
)
geo_lon = models.FloatField(
verbose_name=_("Longitude"),
null=True, blank=True
)
frontpage_text = I18nTextField(
null=True, blank=True,
verbose_name=_("Frontpage text")

View File

@@ -704,6 +704,18 @@ Your {event} team"""))
'default': '',
'type': LazyI18nString
},
'opencagedata_apikey': {
'default': None,
'type': str
},
'leaflet_tiles': {
'default': None,
'type': str
},
'leaflet_tiles_attribution': {
'default': None,
'type': str
},
'frontpage_subevent_ordering': {
'default': 'date_ascending',
'type': str

View File

@@ -112,6 +112,8 @@ class EventWizardBasicsForm(I18nModelForm):
'presale_start',
'presale_end',
'location',
'geo_lat',
'geo_lon',
]
field_classes = {
'date_from': SplitDateTimeField,
@@ -282,6 +284,8 @@ class EventUpdateForm(I18nModelForm):
'presale_start',
'presale_end',
'location',
'geo_lat',
'geo_lon',
]
field_classes = {
'date_from': SplitDateTimeField,

View File

@@ -37,6 +37,20 @@ class GlobalSettingsForm(SettingsForm):
required=False,
label=_("Global message banner detail text"),
)),
('opencagedata_apikey', forms.CharField(
required=False,
label=_("OpenCage API key for geocoding"),
)),
('leaflet_tiles', forms.CharField(
required=False,
label=_("Leaflet tiles URL pattern"),
help_text=_("e.g. {sample}").format(sample="https://a.tile.openstreetmap.org/{z}/{x}/{y}.png")
)),
('leaflet_tiles_attribution', forms.CharField(
required=False,
label=_("Leaflet tiles attribution"),
help_text=_("e.g. {sample}").format(sample='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors')
)),
])
responses = register_global_settings.send(self)
for r, response in sorted(responses, key=lambda r: str(r[0])):

View File

@@ -37,6 +37,8 @@ class SubEventForm(I18nModelForm):
'presale_end',
'location',
'frontpage_text',
'geo_lat',
'geo_lon',
]
field_classes = {
'date_from': SplitDateTimeField,

View File

@@ -50,6 +50,8 @@
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quicksetup.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/dashboard.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/tabs.js" %}"></script>
<script type="text/javascript" src="{% static "leaflet/leaflet.js" %}"></script>
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/geo.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
<script type="text/javascript" src="{% static "sortable/Sortable.js" %}"></script>

View File

@@ -1,6 +1,7 @@
{% extends "pretixcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load static %}
{% load hierarkey_form %}
{% block custom_header %}
{{ block.super }}
@@ -19,7 +20,30 @@
{% bootstrap_field form.slug layout="control" %}
{% bootstrap_field form.date_from layout="control" %}
{% bootstrap_field form.date_to layout="control" %}
{% bootstrap_field form.location layout="control" %}
<div class="geodata-section">
{% bootstrap_field form.location layout="control" %}
<div class="form-group geodata-group" data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}" data-attrib="{{ global_settings.leaflet_tiles_attribution }}" data-icon="{% static "leaflet/images/marker-icon.png" %}" data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
<label class="col-md-3 control-label">
{% trans "Geo coordinates" %}<br>
<span class="optional">{% trans "Optional" %}</span>
</label>
<div class="col-md-4">
{% bootstrap_field form.geo_lat layout="inline" %}
{% if global_settings.opencagedata_apikey %}
<p class="attrib">
<a href="https://openstreetmap.org/" target="_blank">
{% trans "Geocoding data © OpenStreetMap" %}
</a>
</p>
{% endif %}
</div>
<div class="col-md-4">
{% bootstrap_field form.geo_lon layout="inline" %}
</div>
<div class="col-md-1">
</div>
</div>
</div>
{% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.currency layout="control" %}
{% bootstrap_field sform.contact_mail layout="control" %}

View File

@@ -1,6 +1,7 @@
{% extends "pretixcontrol/events/create_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load static %}
{% block form %}
<fieldset>
<legend>{% trans "General information" %}</legend>
@@ -36,7 +37,30 @@
</div>
{% bootstrap_field form.date_from layout="control" %}
{% bootstrap_field form.date_to layout="control" %}
{% bootstrap_field form.location layout="control" %}
<div class="geodata-section">
{% bootstrap_field form.location layout="control" %}
<div class="form-group geodata-group" data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}" data-attrib="{{ global_settings.leaflet_tiles_attribution }}" data-icon="{% static "leaflet/images/marker-icon.png" %}" data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
<label class="col-md-3 control-label">
{% trans "Geo coordinates" %}<br>
<span class="optional">{% trans "Optional" %}</span>
</label>
<div class="col-md-4">
{% bootstrap_field form.geo_lat layout="inline" %}
{% if global_settings.opencagedata_apikey %}
<p class="attrib">
<a href="https://openstreetmap.org/" target="_blank">
{% trans "Geocoding data © OpenStreetMap" %}
</a>
</p>
{% endif %}
</div>
<div class="col-md-4">
{% bootstrap_field form.geo_lon layout="inline" %}
</div>
<div class="col-md-1">
</div>
</div>
</div>
{% bootstrap_field form.currency layout="control" %}
{% bootstrap_field form.tax_rate addon_after="%" layout="control" %}
</fieldset>

View File

@@ -3,6 +3,7 @@
{% load bootstrap3 %}
{% load formset_tags %}
{% load captureas %}
{% load static %}
{% load eventsignal %}
{% block title %}{% trans "Date" context "subevent" %}{% endblock %}
{% block content %}
@@ -266,7 +267,30 @@
{% bootstrap_field form.active layout="control" %}
{% bootstrap_field form.time_from layout="control" %}
{% bootstrap_field form.time_to layout="control" %}
{% bootstrap_field form.location layout="control" %}
<div class="geodata-section">
{% bootstrap_field form.location layout="control" %}
<div class="form-group geodata-group" data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}" data-attrib="{{ global_settings.leaflet_tiles_attribution }}" data-icon="{% static "leaflet/images/marker-icon.png" %}" data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
<label class="col-md-3 control-label">
{% trans "Geo coordinates" %}<br>
<span class="optional">{% trans "Optional" %}</span>
</label>
<div class="col-md-4">
{% bootstrap_field form.geo_lat layout="inline" %}
{% if global_settings.opencagedata_apikey %}
<p class="attrib">
<a href="https://openstreetmap.org/" target="_blank">
{% trans "Geocoding data © OpenStreetMap" %}
</a>
</p>
{% endif %}
</div>
<div class="col-md-4">
{% bootstrap_field form.geo_lon layout="inline" %}
</div>
<div class="col-md-1">
</div>
</div>
</div>
{% bootstrap_field form.time_admission layout="control" %}
{% bootstrap_field form.frontpage_text layout="control" %}
{% bootstrap_field form.is_public layout="control" %}

View File

@@ -3,6 +3,7 @@
{% load bootstrap3 %}
{% load formset_tags %}
{% load eventsignal %}
{% load static %}
{% block title %}{% trans "Date" context "subevent" %}{% endblock %}
{% block content %}
{% if not subevent.pk %}
@@ -24,7 +25,30 @@
{% bootstrap_field form.active layout="control" %}
{% bootstrap_field form.date_from layout="control" %}
{% bootstrap_field form.date_to layout="control" %}
{% bootstrap_field form.location layout="control" %}
<div class="geodata-section">
{% bootstrap_field form.location layout="control" %}
<div class="form-group geodata-group" data-tiles="{{ global_settings.leaflet_tiles|default_if_none:"" }}" data-attrib="{{ global_settings.leaflet_tiles_attribution }}" data-icon="{% static "leaflet/images/marker-icon.png" %}" data-shadow="{% static "leaflet/images/marker-shadow.png" %}">
<label class="col-md-3 control-label">
{% trans "Geo coordinates" %}<br>
<span class="optional">{% trans "Optional" %}</span>
</label>
<div class="col-md-4">
{% bootstrap_field form.geo_lat layout="inline" %}
{% if global_settings.opencagedata_apikey %}
<p class="attrib">
<a href="https://openstreetmap.org/" target="_blank">
{% trans "Geocoding data © OpenStreetMap" %}
</a>
</p>
{% endif %}
</div>
<div class="col-md-4">
{% bootstrap_field form.geo_lon layout="inline" %}
</div>
<div class="col-md-1">
</div>
</div>
</div>
{% bootstrap_field form.date_admission layout="control" %}
{% bootstrap_field form.frontpage_text layout="control" %}
{% bootstrap_field form.is_public layout="control" %}

View File

@@ -1,7 +1,7 @@
from django.conf.urls import include, url
from pretix.control.views import (
auth, checkin, dashboards, event, global_settings, item, main, oauth,
auth, checkin, dashboards, event, geo, global_settings, item, main, oauth,
orders, organizer, pdf, search, shredder, subevents, typeahead, user,
users, vouchers, waitinglist,
)
@@ -22,6 +22,7 @@ urlpatterns = [
url(r'^logdetail/$', global_settings.LogDetailView.as_view(), name='global.logdetail'),
url(r'^logdetail/payment/$', global_settings.PaymentDetailView.as_view(), name='global.paymentdetail'),
url(r'^logdetail/refund/$', global_settings.RefundDetailView.as_view(), name='global.refunddetail'),
url(r'^geocode/$', geo.GeoCodeView.as_view(), name='global.geocode'),
url(r'^reauth/$', user.ReauthView.as_view(), name='user.reauth'),
url(r'^sudo/$', user.StartStaffSession.as_view(), name='user.sudo'),
url(r'^sudo/stop/$', user.StopStaffSession.as_view(), name='user.sudo.stop'),

View File

@@ -0,0 +1,59 @@
import logging
from urllib.parse import quote
import requests
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.cache import cache
from django.http import JsonResponse
from django.views.generic.base import View
from pretix.base.settings import GlobalSettingsObject
logger = logging.getLogger(__name__)
class GeoCodeView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
gs = GlobalSettingsObject()
if not gs.settings.opencagedata_apikey:
return JsonResponse({
'success': False,
'results': []
}, status=200)
q = request.GET.get('q')
cd = cache.get('geocode:{}'.format(q))
if cd:
return JsonResponse({
'success': True,
'results': cd
}, status=200)
try:
r = requests.get(
'https://api.opencagedata.com/geocode/v1/json?q={}&key={}'.format(
quote(q), gs.settings.opencagedata_apikey
)
)
r.raise_for_status()
except IOError:
logger.exception("Geocoding failed")
return JsonResponse({
'success': False,
'results': []
}, status=200)
else:
d = r.json()
res = [
{
'formatted': r['formatted'],
'lat': r['geometry']['lat'],
'lon': r['geometry']['lng'],
} for r in d['results']
]
cache.set('geocode:{}'.format(q), res, timeout=3600 * 6)
return JsonResponse({
'success': True,
'results': res
}, status=200)

View File

@@ -134,6 +134,8 @@ class EventWizard(SafeSessionWizardView):
initial['currency'] = self.clone_from.currency
initial['date_from'] = self.clone_from.date_from
initial['date_to'] = self.clone_from.date_to
initial['geo_lat'] = self.clone_from.geo_lat
initial['geo_lon'] = self.clone_from.geo_lon
initial['presale_start'] = self.clone_from.presale_start
initial['presale_end'] = self.clone_from.presale_end
initial['location'] = self.clone_from.location
@@ -245,6 +247,8 @@ class EventWizard(SafeSessionWizardView):
presale_start=event.presale_start,
presale_end=event.presale_end,
location=event.location,
geo_lat=event.geo_lat,
geo_lon=event.geo_lon,
active=True
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,640 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg,
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-tile {
will-change: opacity;
}
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline: 0;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-container a.leaflet-active {
outline: 2px solid orange;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a,
.leaflet-bar a:hover {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path {
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.7);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover {
text-decoration: underline;
}
.leaflet-container .leaflet-control-attribution,
.leaflet-container .leaflet-control-scale {
font-size: 11px;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: #fff;
background: rgba(255, 255, 255, 0.5);
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 19px;
line-height: 1.4;
}
.leaflet-popup-content p {
margin: 18px 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
padding: 4px 4px 0 0;
border: none;
text-align: center;
width: 18px;
height: 14px;
font: 16px/14px Tahoma, Verdana, sans-serif;
color: #c3c3c3;
text-decoration: none;
font-weight: bold;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover {
color: #999;
}
.leaflet-popup-scrolled {
overflow: auto;
border-bottom: 1px solid #ddd;
border-top: 1px solid #ddd;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-popup-tip-container {
margin-top: -1px;
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-clickable {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}

View File

@@ -0,0 +1,122 @@
/*globals $*/
$(function () {
var j = 0;
$(".geodata-section").each(function () {
// Geocoding
var $sec = $(this);
var $inp = $(this).find("textarea[lang=en], input[lang=en]").first();
if ($inp.length === 0) {
$inp = $(this).find("textarea, input").first();
}
var timer, timer2;
var touched = $sec.find("input[name$=geo_lat]").val() !== "";
function load() {
window.clearTimeout(timer);
var q = $.trim($inp.val().replace(/\n/g, ", "));
if ((touched && $sec.find("input[name$=geo_lat]").val() !== "") || $.trim(q) === "") {
return;
}
$sec.find(".col-md-1").html("<span class='fa fa-cog fa-spin'></span>");
$.getJSON('/control/geocode/?q=' + escape(q), function (res) {
var q2 = $.trim($inp.val().replace(/\n/g, ", "));
if (q2 !== q) {
return; // lost race
}
if (res.results) {
$sec.find("input[name$=geo_lat]").val(res.results[0].lat);
$sec.find("input[name$=geo_lon]").val(res.results[0].lon);
center(13);
}
$sec.find(".col-md-1").html("");
})
}
$sec.find("input[name$=geo_lat], input[name$=geo_lon]").change(function () {
touched = $sec.find("input[name$=geo_lat]").val() !== "";
center(13);
}).keyup(function () {
if (timer2) {
window.clearTimeout(timer2);
}
timer2 = window.setTimeout(center, 300);
});
$inp.change(load);
$inp.keyup(function () {
if (timer) {
window.clearTimeout(timer);
}
timer = window.setTimeout(load, 1000);
});
// Map
var $grp = $sec.find(".geodata-group");
var tiles = $grp.attr("data-tiles");
var attrib = $grp.attr("data-attrib");
if (tiles) {
var $map = $("<div>");
$grp.append($("<div>").addClass("col-md-9 col-md-offset-3").append($map));
var map = L.map($map.get(0));
L.tileLayer(tiles, {
attribution: attrib,
maxZoom: 18,
}).addTo(map);
var $lat = $sec.find("input[name$=geo_lat]");
var $lon = $sec.find("input[name$=geo_lon]");
function getpoint() {
if ($lat.val() !== "" && $lon.val() !== "") {
return [parseFloat($lat.val().replace(",", ".")), parseFloat($lon.val().replace(",", "."))];
} else {
return [0.0, 0.0];
}
}
var marker = L.marker(getpoint(), {
draggable: 'true',
icon: L.icon({
iconUrl: $grp.attr("data-icon"),
shadowUrl: $grp.attr("data-shadow"),
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
tooltipAnchor: [16, -28],
shadowSize: [41, 41]
})
});
marker.addTo(map);
marker.on("dragend", function (event) {
var position = marker.getLatLng();
marker.setLatLng(position, {
draggable: 'true'
}).bindPopup(position).update();
$lat.val(position.lat.toFixed(7));
$lon.val(position.lng.toFixed(7));
center(null);
});
function center(zoom) {
if ($lat.val() !== "" && $lon.val() !== "") {
if (zoom) {
map.setView(getpoint(), zoom);
} else {
map.panTo(getpoint());
}
marker.setLatLng(getpoint(), {
draggable: 'true'
}).bindPopup(getpoint()).update();
} else {
map.fitWorld();
}
}
center(13);
} else {
function center(zoom) {
}
}
});
});

View File

@@ -224,6 +224,23 @@ div.mail-preview {
input[type=number].short {
width: 80px !important;
}
.geodata-group {
.form-group {
margin: 0 0 10px 0;
}
.attrib {
padding: 0;
margin: 0;
font-size: $font-size-small;
a {
color: $text-muted;
}
}
.leaflet-container {
height: 200px;
margin: 0;
}
}
.metadata-group {
.form-group {
margin: 0 0 10px 0;

View File

@@ -12,6 +12,7 @@
@import "_dashboard.scss";
@import "../../pretixbase/scss/webfont.scss";
@import "../../fileupload/jquery.fileupload.scss";
@import "../../leaflet/leaflet.scss";
@import "../../select2/select2.scss";
@import "../../select2/select2_bootstrap.scss";
@import "../../colorpicker/bootstrap-colorpicker.scss";