implement label settings

This commit is contained in:
Laura Klünder 2019-12-22 00:38:54 +01:00
parent 3ce5e663c5
commit ecd7cc5637
8 changed files with 180 additions and 33 deletions

View file

@ -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]

View file

@ -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'))

View file

@ -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'))

View file

@ -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'),
),
]

View file

@ -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'

View file

@ -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 ()

View file

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

View file

@ -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 = $('<div class="location-label-text">').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: $('<div class="location-label-text">').text(location.title)[0].outerHTML,
html: html[0].outerHTML,
iconSize: null,
className: 'location-label'
}),