Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ĝisdatigoj por dinamikaj kaj statikaj mapoj (parto 2) #325

Merged
merged 7 commits into from
Jan 20, 2024
4 changes: 4 additions & 0 deletions core/static/js/forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ $(function() {
constraint_failed = true;
}
}
if ($this.attr('data-complex-validation-failed')) {
errors.push($this.attr('data-complex-validation-failed'));
constraint_failed = true;
}

if (constraint_failed) {
this.setCustomValidity(errors.join("\n"));
Expand Down
57 changes: 57 additions & 0 deletions core/static/js/place-location.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// @source: https://github.com/tejo-esperanto/pasportaservo/blob/master/core/static/js/place-location.js
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL v3


$(function() {

$('#id_coordinates').on('input', function(event) {
this.removeAttribute('data-complex-validation-failed');
if (typeof this.mapref !== "undefined" && this.value.trim()) {
var field = this, coordinates, lnglat;
if (typeof field.pattern !== "undefined") {
// constraint validation happens only after this handler, so it is better to
// check explicitely the validity of the typed (or pasted) input.
if (new RegExp(field.pattern).test(field.value))
coordinates = field.value;
}
else {
// no pattern defined: we must work with possibly bad data. it will cause an
// exception when trying to convert to the `LngLat` object.
coordinates = field.value;
}
if (!coordinates)
return;
coordinates = coordinates.trim().replace(/(\s+,?\s*|\s*,?\s+)/, ",").split(",");
try {
lnglat = mapboxgl.LngLat.convert([coordinates[1], coordinates[0]]).wrap();
}
catch (e) {
field.setAttribute('data-complex-validation-failed', gettext("Bad or impossible coordinates."));
return;
}
var lng_precision = coordinates[1].indexOf(".") >= 0 ? coordinates[1].split(".")[1].length : 0,
lat_precision = coordinates[0].indexOf(".") >= 0 ? coordinates[0].split(".")[1].length : 0,
precision = Math.min(lng_precision, lat_precision);
var zoomLevel;
// the decimal precision defines the zoom level of the map -- that is, how many
// details are visible, -- which in turn defines if the value can be submitted.
// see https://docs.mapbox.com/help/glossary/zoom-level/
// and https://en.wikipedia.org/wiki/Decimal_degrees#Precision
if (precision <= 6) {
zoomLevel = Math.min([0, 3, 6, 9, 13, 16, 19][precision], field.mapref.getMaxZoom());
}
else {
zoomLevel = field.mapref.getMaxZoom();
}
field.mapClick({lngLat: lnglat}, true, zoomLevel);
}
});

if (!$('#id_coordinates').closest('.form-group').hasClass('has-error')) {
$('#id_coordinates').siblings('[id^="hint_"]').first().prepend(gettext("Alternatively: "));
}

});


// @license-end
13 changes: 12 additions & 1 deletion core/static/sass/_all.scss
Original file line number Diff line number Diff line change
Expand Up @@ -748,10 +748,21 @@ html:not(.msie-compat) .mapboxgl-canvas[tabindex="0"]:focus {
outline-offset: -1px;
outline-color: #0096ff;
}
#page.place-print #map {
#page.place-print #map, #page.place-print #static_map {
border: solid 1px $color-gray-light;
border-radius: 5px;
}
.static-map-auto-switch {
font-size: 13px !important;
font-style: italic;
text-align: justify;
color: $color-text;

&.empty {
padding: 0;
margin: 0;
}
}


/* Profile */
Expand Down
6 changes: 6 additions & 0 deletions core/templatetags/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ def public_id(account):
register.simple_tag(func=lambda **kwargs: dict(kwargs), name='dict')


@register.simple_tag
def dict_insert(d: dict, key: Any, value: Any):
d[key] = value
return ''


@register.filter(is_safe=True)
def are_any(iterable):
try:
Expand Down
85 changes: 81 additions & 4 deletions hosting/forms/places.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
from collections import namedtuple
from datetime import date
from itertools import groupby
from typing import cast

from django import forms
from django.contrib.auth import get_user_model
from django.contrib.gis.geos import LineString, Point
from django.contrib.gis.geos import GEOSGeometry, LineString, Point
from django.core.validators import RegexValidator
from django.db.models import Case, When
from django.db.models.fields import BLANK_CHOICE_DASH
from django.utils.functional import keep_lazy_text, lazy
Expand Down Expand Up @@ -397,21 +399,96 @@ class Meta:
model = Place
fields = ['location']
widgets = {
'location': MapboxGlWidget(),
'location': MapboxGlWidget({'has_input_fallback': True}),
}

lnglat_pair_re = r'\A\s*-?\d+(\.\d+)?\s*,?\s*-?\d+(\.\d+)?\s*\Z'

coordinates = forms.CharField(
label=_("Coordinates"),
required=False,
validators=[RegexValidator(lnglat_pair_re)],
error_messages={
'invalid': _("Improperly formatted or invalid pair of coordinates."),
},
help_text=_("Type the decimal latitude (horizontal line, <nobr>-90°−90°</nobr>) "
"and longitude (vertical line, <nobr>-180°−180°</nobr>)."))

def __init__(self, *args, **kwargs):
self.view_role = kwargs.pop('view_role')
super().__init__(*args, **kwargs)
self.fields['location'].widget.attrs['data-selectable-zoom'] = 11.5
if self.initial.get('location') and not self.initial['location'].empty:
self.fields['coordinates'].initial = (
'{coords[1]}, {coords[0]}'.format(coords=self.initial['location'].coords)
)
self.fields['coordinates'].widget.attrs['pattern'] = (
# JavaScript's RegExes have more limited functionality compared to Python.
self.lnglat_pair_re.replace(r'\A', '^').replace(r'\Z', '$')
)

def clean_coordinates(self):
# The coordinates might be empty; in that case there is no data to be
# processed further.
if not self.cleaned_data['coordinates']:
return None
# Try splitting the coordinates string into a pair of decimal degrees
# for latitude and longitude, and converting to a GEOS Point.
# The coordinates might be separated by a comma or a series of spaces.
try:
coords = re.sub(r'(\s+,?\s*|\s*,?\s+)', ',', self.cleaned_data['coordinates'])
coords = coords.split(',')
geometry = cast(
Point,
GEOSGeometry(f'SRID={SRID};POINT({coords[1]} {coords[0]})'))
except Exception as e:
logging.getLogger('PasportaServo.geo').warning(
f"Invalid coordinates \"{self.cleaned_data['coordinates']}\": {e}",
exc_info=e)
raise forms.ValidationError(
self.fields['coordinates'].error_messages['invalid'],
code='invalid')

# The number of decimal places determines the precision of the location
# given (see https://en.wikipedia.org/wiki/Decimal_degrees#Precision).
def calc_precision(value):
if int(value) == value:
return 0
else:
return len(str(value).split('.')[1])
setattr(
geometry,
'decimal_precision',
min(calc_precision(geometry.x), calc_precision(geometry.y))
)

# Both latitude (y) and longitude (x) values need manual wrapping,
# to ensure that they are in the -90° to 90° and -180° to 180° range,
# correspondingly.
while geometry.x < -180:
geometry.x += 360
while geometry.x > 180:
geometry.x -= 360
while geometry.y < -90:
geometry.y = -180 - geometry.y
while geometry.y > 90:
geometry.y = 180 - geometry.y
return geometry

def save(self, commit=True):
place = super().save(commit=False)
if self.cleaned_data.get('location'):
place.location = self.cleaned_data.get('coordinates')
if place.location:
if self.view_role >= AuthRole.SUPERVISOR:
place.location_confidence = LocationConfidence.CONFIRMED
else:
place.location_confidence = LocationConfidence.EXACT
match self.cleaned_data['coordinates'].decimal_precision:
case digits if digits >= 4:
place.location_confidence = LocationConfidence.EXACT
case digits if digits >= 1:
place.location_confidence = LocationConfidence.LT_25KM
case _:
place.location_confidence = LocationConfidence.GT_25KM
else:
place.location_confidence = LocationConfidence.UNDETERMINED
if commit:
Expand Down
21 changes: 21 additions & 0 deletions hosting/migrations/0067_remove_place_lat_lng.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 3.2.20 on 2024-01-17 20:37

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('hosting', '0066_hosting_conditions'),
]

operations = [
migrations.RemoveField(
model_name='place',
name='latitude',
),
migrations.RemoveField(
model_name='place',
name='longitude',
),
]
31 changes: 0 additions & 31 deletions hosting/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -726,12 +726,6 @@ class Place(TrackingModel, TimeStampedModel):
location_confidence = models.PositiveSmallIntegerField(
_("confidence"),
default=0)
latitude = models.FloatField(
_("latitude"),
null=True, blank=True)
longitude = models.FloatField(
_("longitude"),
null=True, blank=True)
max_guest = RangeIntegerField(
_("maximum number of guests"),
min_value=1, max_value=50,
Expand Down Expand Up @@ -817,31 +811,6 @@ def profile(self):
"""Proxy for self.owner. Rename 'owner' to 'profile' if/as possible."""
return self.owner

@property
def lat(self):
if not self.location or self.location.empty:
return 0
return round(self.location.y, 2)

@property
def lng(self):
if not self.location or self.location.empty:
return 0
return round(self.location.x, 2)

@property
def bbox(self):
"""
Returns an OpenStreetMap-formatted bounding box.
See http://wiki.osm.org/wiki/Bounding_Box
"""
dx, dy = 0.007, 0.003 # Delta lng and delta lat around position
if self.location and not self.location.empty:
boundingbox = (self.lng - dx, self.lat - dy, self.lng + dx, self.lat + dy)
return ",".join([str(coord) for coord in boundingbox])
else:
return ""

@cached_property
def subregion(self):
try:
Expand Down
51 changes: 12 additions & 39 deletions hosting/templates/hosting/place_detail.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends 'core/base.html' %}
{% load i18n l10n static cache solo_tags geojson_tags geoformat %}
{% load i18n static cache %}
{% load profile privacy expression variable utils %}

{% block head_title %}
Expand Down Expand Up @@ -349,49 +349,22 @@ <h3>{% trans "Family members" %}</h3>
</a>
</div>
{% endif %}
{% include './snippets/place_map.html' with static_style='clpi3cepm00jy01qt2yyx7aik' %}
{% if not simple_map %}
<div class="embed-responsive embed-responsive-16by9" id="map"
{% if place_location.coords %}
data-marker="{{ place_location.coords|geojsonfeature }}"
{% endif %}
{% if place_location.bounds %}
data-bounds="{{ place_location.bounds|geojsonfeature }}"
{% endif %}
data-marker-type="{{ place_location.type }}">
{% comment %} responsive map height with constant ratio to map width {% endcomment %}
</div>
<noscript>
<style>#map { display: none; }</style>
{% comment %} the static map will be displayed when scripting is not possible {% endcomment %}
{% endif %}
{% dict fill="#1bf" fill__opacity=0.25 stroke="#eee" crs=False as circle_style %}
<img style="width: 100%" src="{% filter compact|cut:" " %}{% localize off %}
https://api.mapbox.com/styles/v1/pasportaservo/clpi3cepm00jy01qt2yyx7aik/static
{% if place_location.type == 'P' %}
/pin-s+F71({{ place_location.coords.x }},{{ place_location.coords.y }})
{% elif place_location.type == 'C' %}
/geojson({{ place_location.box|geojsonfeature|geojsonfeature_styling:circle_style|cut:" "|urlencode }})
{% endif %}
{% if place_location.coords %}
/{{ place_location.coords.x }},{{ place_location.coords.y }},
{% if place_location.type == 'P' %}17{% else %}14{% endif %}
{% elif place_location.bounds %}
/{{ place_location.bounds.0.geom.x }},{{ place_location.bounds.0.geom.y }},8
{% else %}
/10,20,1
{% endif %}
{% get_solo 'core.SiteConfiguration' as site_config %}
/720x400@2x?attribution=false&amp;access_token={{ site_config.mapping_services_api_keys.mapbox }}
{% endlocalize %}{% endfilter %}"
alt="[{% trans "Location on map" %}]" />
{% if not simple_map %}
</noscript>
{% if user.is_authenticated %}
<mark class="help-block static-map-auto-switch empty hidden-print"
data-notification="{% blocktrans trimmed %}
Simple map is shown: the WebGL technology, needed for the fully capable map,
is not available in your browser.
{% endblocktrans %}">
</mark>
{% endif %}
{% endif %}
<form action="{% url 'map_type_setup' simple_map|yesno:'3,0' %}" method="POST"
class="btn-toolbar pull-right requires-scripting hidden-print" data-nosnippet>
class="btn-toolbar pull-right hidden-print" data-nosnippet>
{% csrf_token %}
<input type="hidden" name="{{ REDIRECT_FIELD_NAME }}" value="{{ request.get_full_path }}" />
<button type="submit" class="btn btn-default btn-xs btn-vert-space-even ajax"
<button type="submit" class="btn btn-default btn-xs btn-vert-space-even requires-scripting ajax"
data-csrf="{{ csrf_token }}"
data-on-ajax-success="setupMapStyleSuccess">
{% if not simple_map %}{% trans "Use a simple map" %}{% else %}{% trans "Use a fully-capable map" %}{% endif %}
Expand Down
3 changes: 3 additions & 0 deletions hosting/templates/hosting/place_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
{% block extra_js %}
{{ block.super }}
<script src="{% static 'chosen/chosen.jquery.min.js' %}" type="text/javascript"></script>
{% if form.location %}
<script src="{% static 'js/place-location.js' %}" type="text/javascript"></script>
{% endif %}
{% endblock %}


Expand Down
Loading