From ecd7cc56371fa4f5c38ee2eb128b4ef8c3420914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Sun, 22 Dec 2019 00:38:54 +0100 Subject: [PATCH] implement label settings --- src/c3nav/editor/forms.py | 15 ++- src/c3nav/editor/urls.py | 1 + src/c3nav/editor/views/edit.py | 1 + .../mapdata/migrations/0075_label_settings.py | 97 +++++++++++++++++++ src/c3nav/mapdata/models/locations.py | 78 ++++++++++----- src/c3nav/mapdata/utils/json.py | 2 + src/c3nav/site/static/site/css/c3nav.scss | 4 + src/c3nav/site/static/site/js/c3nav.js | 15 ++- 8 files changed, 180 insertions(+), 33 deletions(-) create mode 100644 src/c3nav/mapdata/migrations/0075_label_settings.py diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index fa002845..ea434089 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -3,6 +3,7 @@ import operator import os from functools import reduce from itertools import chain +from operator import attrgetter from django.conf import settings from django.core.cache import cache @@ -124,8 +125,18 @@ class EditorFormBase(I18nModelFormMixin, ModelForm): help_text=category.help_text) self.fields[name] = field + if 'label_settings' in self.fields: + self.fields.move_to_end('label_settings') + + for field in tuple(self.fields.keys()): + if field.startswith('label_override'): + self.fields.move_to_end(field) + if 'category' in self.fields: - self.fields['category'].label_from_instance = lambda obj: obj.title + self.fields['category'].label_from_instance = attrgetter('title') + + if 'label_settings' in self.fields: + self.fields['label_settings'].label_from_instance = attrgetter('title') if 'access_restriction' in self.fields: AccessRestriction = self.request.changeset.wrap_model('AccessRestriction') @@ -268,12 +279,12 @@ class EditorFormBase(I18nModelFormMixin, ModelForm): def create_editor_form(editor_model): possible_fields = ['slug', 'name', 'title', 'title_plural', 'help_text', 'icon', 'join_edges', 'up_separate', 'walk', 'ordering', 'category', 'width', 'groups', 'color', 'priority', 'hierarchy', 'icon_name', - 'show_labels', 'show_label', 'base_altitude', 'waytype', 'access_restriction', 'height', 'default_height', 'door_height', 'outside', 'can_search', 'can_describe', 'geometry', 'single', 'altitude', 'short_label', 'origin_space', 'target_space', 'data', 'comment', 'slow_down_factor', 'extra_seconds', 'speed', 'description', 'speed_up', 'description_up', 'enter_description', 'level_change_description', 'base_mapdata_accessible', + 'label_settings', 'label_override', 'min_zoom', 'max_zoom', 'font_size', 'allow_levels', 'allow_spaces', 'allow_areas', 'allow_pois', 'left', 'top', 'right', 'bottom'] field_names = [field.name for field in editor_model._meta.get_fields() if not field.one_to_many] existing_fields = [name for name in possible_fields if name in field_names] diff --git a/src/c3nav/editor/urls.py b/src/c3nav/editor/urls.py index 8a591c8a..4549fea9 100644 --- a/src/c3nav/editor/urls.py +++ b/src/c3nav/editor/urls.py @@ -57,6 +57,7 @@ urlpatterns.extend(add_editor_urls('WayType')) urlpatterns.extend(add_editor_urls('AccessRestriction')) urlpatterns.extend(add_editor_urls('AccessRestrictionGroup')) urlpatterns.extend(add_editor_urls('Source')) +urlpatterns.extend(add_editor_urls('LabelSettings')) urlpatterns.extend(add_editor_urls('Building', 'Level')) urlpatterns.extend(add_editor_urls('Space', 'Level', explicit_edit=True)) urlpatterns.extend(add_editor_urls('Door', 'Level')) diff --git a/src/c3nav/editor/views/edit.py b/src/c3nav/editor/views/edit.py index eeaff70f..f4db125c 100644 --- a/src/c3nav/editor/views/edit.py +++ b/src/c3nav/editor/views/edit.py @@ -55,6 +55,7 @@ def main_index(request): child_model(request, 'WayType'), child_model(request, 'AccessRestriction'), child_model(request, 'AccessRestrictionGroup'), + child_model(request, 'LabelSettings'), child_model(request, 'Source'), ], }, fields=('can_create_level', 'child_models')) diff --git a/src/c3nav/mapdata/migrations/0075_label_settings.py b/src/c3nav/mapdata/migrations/0075_label_settings.py new file mode 100644 index 00000000..5a20f6e5 --- /dev/null +++ b/src/c3nav/mapdata/migrations/0075_label_settings.py @@ -0,0 +1,97 @@ +# Generated by Django 2.2.8 on 2019-12-21 23:27 + +import c3nav.mapdata.fields +from decimal import Decimal +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0074_show_labels'), + ] + + operations = [ + migrations.CreateModel( + name='LabelSettings', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', c3nav.mapdata.fields.I18nField(fallback_any=True, plural_name='titles', verbose_name='Title')), + ('min_zoom', models.DecimalField(decimal_places=1, default=-10, max_digits=3, validators=[django.core.validators.MinValueValidator(Decimal('-10')), django.core.validators.MaxValueValidator(Decimal('10'))], verbose_name='min zoom')), + ('max_zoom', models.DecimalField(decimal_places=1, default=10, max_digits=3, validators=[django.core.validators.MinValueValidator(Decimal('-10')), django.core.validators.MaxValueValidator(Decimal('10'))], verbose_name='max zoom')), + ('font_size', models.IntegerField(default=12, validators=[django.core.validators.MinValueValidator(12), django.core.validators.MaxValueValidator(30)], verbose_name='font size')), + ], + options={ + 'verbose_name': 'Label Settings', + 'verbose_name_plural': 'Label Settings', + 'default_related_name': 'labelsettings', + }, + ), + migrations.RemoveField( + model_name='area', + name='show_label', + ), + migrations.RemoveField( + model_name='level', + name='show_label', + ), + migrations.RemoveField( + model_name='locationgroup', + name='show_labels', + ), + migrations.RemoveField( + model_name='poi', + name='show_label', + ), + migrations.RemoveField( + model_name='space', + name='show_label', + ), + migrations.AddField( + model_name='area', + name='label_override', + field=c3nav.mapdata.fields.I18nField(blank=True, fallback_any=True, plural_name='label_overrides', verbose_name='Label override'), + ), + migrations.AddField( + model_name='level', + name='label_override', + field=c3nav.mapdata.fields.I18nField(blank=True, fallback_any=True, plural_name='label_overrides', verbose_name='Label override'), + ), + migrations.AddField( + model_name='poi', + name='label_override', + field=c3nav.mapdata.fields.I18nField(blank=True, fallback_any=True, plural_name='label_overrides', verbose_name='Label override'), + ), + migrations.AddField( + model_name='space', + name='label_override', + field=c3nav.mapdata.fields.I18nField(blank=True, fallback_any=True, plural_name='label_overrides', verbose_name='Label override'), + ), + migrations.AddField( + model_name='area', + name='label_settings', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='areas', to='mapdata.LabelSettings', verbose_name='label settings'), + ), + migrations.AddField( + model_name='level', + name='label_settings', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='levels', to='mapdata.LabelSettings', verbose_name='label settings'), + ), + migrations.AddField( + model_name='locationgroup', + name='label_settings', + field=models.ForeignKey(help_text='unless location specifies otherwise', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='locationgroups', to='mapdata.LabelSettings', verbose_name='label settings'), + ), + migrations.AddField( + model_name='poi', + name='label_settings', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pois', to='mapdata.LabelSettings', verbose_name='label settings'), + ), + migrations.AddField( + model_name='space', + name='label_settings', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='spaces', to='mapdata.LabelSettings', verbose_name='label settings'), + ), + ] diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index ec4567b5..cb953d19 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -1,7 +1,8 @@ from contextlib import suppress +from decimal import Decimal from operator import attrgetter -from django.core.validators import RegexValidator +from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator from django.db import models from django.db.models import FieldDoesNotExist, Prefetch from django.urls import reverse @@ -103,7 +104,7 @@ class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model): result = super().serialize(detailed=detailed, **kwargs) if not detailed: fields = ('id', 'type', 'slug', 'title', 'subtitle', 'icon', 'point', 'bounds', 'grid_square', - 'locations', 'on_top_of', 'show_label') + 'locations', 'on_top_of', 'label_settings', 'label_override') result = {name: result[name] for name in fields if name in result} return result @@ -158,14 +159,10 @@ class Location(LocationSlug, AccessRestrictionMixin, TitledMixin, models.Model): class SpecificLocation(Location, models.Model): - SHOW_LABEL_OPTIONS = ( - ('inherit', _('inherit from groups (default)')), - ('show_text', _('yes, show the title')), - ('no', _('don\'t show')), - ) - groups = models.ManyToManyField('mapdata.LocationGroup', verbose_name=_('Location Groups'), blank=True) - show_label = models.CharField(_('show label'), max_length=16, default='inherit', choices=SHOW_LABEL_OPTIONS) + label_settings = models.ForeignKey('mapdata.LabelSettings', null=True, on_delete=models.PROTECT, + verbose_name=_('label settings')) + label_override = I18nField(_('Label override'), plural_name='label_overrides', blank=True, fallback_any=True) class Meta: abstract = True @@ -184,18 +181,22 @@ class SpecificLocation(Location, models.Model): for category, items in groups.items() if getattr(category, 'allow_'+self.__class__._meta.default_related_name)} result['groups'] = groups - result['show_label'] = self.get_show_label() + + label_settings = self.get_label_settings() + if label_settings: + result['label_settings'] = label_settings.serialize(detailed=False) + if self.label_overrides: + # todo: what if only one language is set? + result['label_override'] = self.label_override return result - def get_show_label(self): - if self.show_label == 'inherit': - for group in self.groups.all(): - if group.show_labels != 'no': - return group.show_labels - return None - if self.show_label == 'no': - return None - return self.show_label + def get_label_settings(self): + if self.label_settings: + return self.label_settings + for group in self.groups.all(): + if group.label_settings: + return group.label_settings + return None def details_display(self, **kwargs): result = super().details_display(**kwargs) @@ -304,17 +305,13 @@ class LocationGroupManager(models.Manager): class LocationGroup(Location, models.Model): - SHOW_LABELS_OPTIONS = ( - ('no', _('no (default)')), - ('show_text', _('yes, show the title')), - ) - category = models.ForeignKey(LocationGroupCategory, related_name='groups', on_delete=models.PROTECT, verbose_name=_('Category')) priority = models.IntegerField(default=0, db_index=True) hierarchy = models.IntegerField(default=0, db_index=True, verbose_name=_('hierarchy')) - show_labels = models.CharField(_('show labels'), max_length=16, default='no', choices=SHOW_LABELS_OPTIONS, - help_text=_('unless location specifies otherwise')) + label_settings = models.ForeignKey('mapdata.LabelSettings', null=True, on_delete=models.PROTECT, + verbose_name=_('label settings'), + help_text=_('unless location specifies otherwise')) color = models.CharField(null=True, blank=True, max_length=32, verbose_name=_('background color')) objects = LocationGroupManager() @@ -418,3 +415,32 @@ class LocationRedirect(LocationSlug): class Meta: default_related_name = 'redirect' + + +class LabelSettings(SerializableMixin, models.Model): + title = I18nField(_('Title'), plural_name='titles', fallback_any=True) + min_zoom = models.DecimalField(_('min zoom'), max_digits=3, decimal_places=1, default=-10, + validators=[MinValueValidator(Decimal('-10')), + MaxValueValidator(Decimal('10'))]) + max_zoom = models.DecimalField(_('max zoom'), max_digits=3, decimal_places=1, default=10, + validators=[MinValueValidator(Decimal('-10')), + MaxValueValidator(Decimal('10'))]) + font_size = models.IntegerField(_('font size'), default=12, + validators=[MinValueValidator(12), + MaxValueValidator(30)]) + + def _serialize(self, detailed=True, **kwargs): + result = super()._serialize(detailed=detailed, **kwargs) + if detailed: + result['titles'] = self.titles + if self.min_zoom > -10: + result['min_zoom'] = self.min_zoom + if self.max_zoom < 10: + result['max_zoom'] = self.max_zoom + result['font_size'] = self.font_size + return result + + class Meta: + verbose_name = _('Label Settings') + verbose_name_plural = _('Label Settings') + default_related_name = 'labelsettings' diff --git a/src/c3nav/mapdata/utils/json.py b/src/c3nav/mapdata/utils/json.py index b40d8bf7..fe322de3 100644 --- a/src/c3nav/mapdata/utils/json.py +++ b/src/c3nav/mapdata/utils/json.py @@ -78,6 +78,8 @@ def round_polygon(coordinates): # round each ring on it's own and remove rings that are invalid # if the exterior ring is invalid, return and empty polygon coordinates = tuple(round_coordinates(ring) for ring in coordinates) + if not coordinates: + return coordinates exterior, *interiors = coordinates if not check_ring(exterior): return () diff --git a/src/c3nav/site/static/site/css/c3nav.scss b/src/c3nav/site/static/site/css/c3nav.scss index e30dae3e..a7a897ea 100644 --- a/src/c3nav/site/static/site/css/c3nav.scss +++ b/src/c3nav/site/static/site/css/c3nav.scss @@ -579,6 +579,10 @@ main.show-options #resultswrapper #route-options { white-space: nowrap; } .location-label-text { + background-color: rgba(255, 255, 255, 0.6); + line-height: 100%; + padding: 2px 3px; + border-radius: 2px; white-space: nowrap; transform: translateX(-50%) translateY(-50%); } diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index fe3829ce..663a2879 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -108,7 +108,7 @@ c3nav = { location.match = ' ' + location.title_words.join(' ') + ' ' + location.subtitle_words.join(' ') + ' ' + location.slug; locations.push(location); locations_by_id[location.id] = location; - if (location.point && location.show_label) { + if (location.point && location.label_settings) { location.label = c3nav._build_location_label(location); if (!(location.point[0] in labels)) labels[location.point[0]] = []; labels[location.point[0]].push(location); @@ -303,12 +303,15 @@ c3nav = { update_location_labels: function() { c3nav._labelLayer.clearLayers(); var labels = c3nav.labels[c3nav._levelControl.currentLevel], - bounds = c3nav.map.getBounds(); + bounds = c3nav.map.getBounds(), + zoom = c3nav.map.getZoom(); if (!labels) return; for (var location of labels) { - if (bounds.contains(location.label.getLatLng())) { - c3nav._labelLayer._maybeAddLayerToRBush(location.label); + if (bounds.contains(location.label.getLatLng()) && + (location.label_settings.min_zoom || -10) < zoom && + (location.label_settings.max_zoom || 10) > zoom) { + c3nav._labelLayer._maybeAddLayerToRBush(location.label); } } }, @@ -801,9 +804,11 @@ c3nav = { return html[0].outerHTML; }, _build_location_label: function(location) { + var html = $('
').text(location.label_override || location.title); + html.css('font-size', location.label_settings.font_size+'px'); return L.marker(L.GeoJSON.coordsToLatLng(location.point.slice(1)), { icon: L.divIcon({ - html: $('
').text(location.title)[0].outerHTML, + html: html[0].outerHTML, iconSize: null, className: 'location-label' }),