From fe98f5e618670b310b22c90f1f4279ed4c16dca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Thu, 19 Dec 2024 10:56:38 +0100 Subject: [PATCH] add level_index so short_label can look nicer --- src/c3nav/editor/forms.py | 6 +-- .../mapdata/management/commands/rendermap.py | 6 +-- ...vel_level_index_alter_level_short_label.py | 38 +++++++++++++++++++ src/c3nav/mapdata/models/level.py | 17 ++++++++- src/c3nav/mapdata/render/engines/blender.py | 2 +- src/c3nav/mapdata/render/geometry/level.py | 2 + src/c3nav/mapdata/render/renderdata.py | 1 + src/c3nav/mapdata/render/renderer.py | 2 +- src/c3nav/mapdata/schemas/models.py | 4 ++ src/c3nav/mapdata/utils/cache/stats.py | 12 +++--- src/c3nav/mapdata/utils/locations.py | 10 ++--- src/c3nav/site/converters.py | 4 +- src/c3nav/site/static/site/js/c3nav.js | 10 ++--- src/c3nav/site/views.py | 6 +-- 14 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 src/c3nav/mapdata/migrations/0121_level_level_index_alter_level_short_label.py diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index dc1a0166..66b8dbe5 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -401,9 +401,9 @@ def create_editor_form(editor_model): 'slug', 'name', 'title', 'title_plural', 'help_text', 'position_secret', 'icon', 'join_edges', 'up_separate', 'bssid', 'main_point', 'external_url', 'hub_import_type', 'walk', 'ordering', 'category', 'width', 'groups', 'height', 'color', 'in_legend', 'priority', 'hierarchy', 'icon_name', - 'base_altitude', 'intermediate', - 'waytype', 'access_restriction', 'default_height', 'door_height', 'outside', 'can_search', - 'can_describe', 'geometry', 'single', 'altitude', 'short_label', 'origin_space', 'target_space', 'data', + 'base_altitude', 'intermediate', 'waytype', 'access_restriction', 'default_height', 'door_height', 'outside', + 'can_search', 'can_describe', 'geometry', 'single', 'altitude', 'level_index', 'short_label', + 'origin_space', 'target_space', 'data', 'comment', 'slow_down_factor', 'groundaltitude', 'node_number', 'wifi_bssid', 'bluetooth_address', "group", 'ibeacon_uuid', 'ibeacon_major', 'ibeacon_minor', 'uwb_address', 'extra_seconds', 'speed', 'can_report_missing', "can_report_mistake", 'description', 'speed_up', 'description_up', 'avoid_by_default', 'report_help_text', diff --git a/src/c3nav/mapdata/management/commands/rendermap.py b/src/c3nav/mapdata/management/commands/rendermap.py index 3bdbb0b3..e7f609bb 100644 --- a/src/c3nav/mapdata/management/commands/rendermap.py +++ b/src/c3nav/mapdata/management/commands/rendermap.py @@ -20,9 +20,9 @@ class Command(BaseCommand): return Level.objects.filter(on_top_of__isnull=True) values = set(v for v in value.split(',') if v) - levels = Level.objects.filter(on_top_of__isnull=True, short_label__in=values) + levels = Level.objects.filter(on_top_of__isnull=True, level___in=values) - not_found = values - set(level.short_label for level in levels) + not_found = values - set(level.level_index for level in levels) if not_found: raise argparse.ArgumentTypeError( ngettext_lazy('Unknown level: %s', 'Unknown levels: %s', len(not_found)) % ', '.join(not_found) @@ -122,7 +122,7 @@ class Command(BaseCommand): scale=options['scale'], full_levels=options['full_levels'], min_width=options['min_width']) - name = options['name'] or ('level_%s' % level.short_label) + name = options['name'] or ('level_%s' % level.level_index) filename = settings.RENDER_ROOT / ('%s.%s' % (name, options['filetype'])) if options['filetype'] == 'svg': diff --git a/src/c3nav/mapdata/migrations/0121_level_level_index_alter_level_short_label.py b/src/c3nav/mapdata/migrations/0121_level_level_index_alter_level_short_label.py new file mode 100644 index 00000000..3737a69e --- /dev/null +++ b/src/c3nav/mapdata/migrations/0121_level_level_index_alter_level_short_label.py @@ -0,0 +1,38 @@ +# Generated by Django 5.0.8 on 2024-12-19 08:18 + +import django.core.validators +import re +from django.db import migrations, models + + +def set_level_index(apps, schema_editor): + Level = apps.get_model('mapdata', 'Level') + for level in Level.objects.all(): + level.level_index = level.short_label + level.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('mapdata', '0120_level_intermediate'), + ] + + operations = [ + migrations.AddField( + model_name='level', + name='level_index', + field=models.CharField(help_text='used for coordinates', max_length=20, null=True, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9._]+\\Z'), 'Enter a valid “level index” consisting of letters, numbers, underscores, dots or hyphens.', 'invalid')], verbose_name='level index') + ), + migrations.AlterField( + model_name='level', + name='short_label', + field=models.CharField(help_text='used for the level selector', max_length=20, unique=True, verbose_name='short label'), + ), + migrations.RunPython(set_level_index, migrations.RunPython.noop), + migrations.AlterField( + model_name='level', + name='level_index', + field=models.CharField(help_text='used for coordinates', max_length=20, unique=True, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9._]+\\Z'), 'Enter a valid “level index” consisting of letters, numbers, underscores, dots or hyphens.', 'invalid')], verbose_name='level index') + ), + ] diff --git a/src/c3nav/mapdata/models/level.py b/src/c3nav/mapdata/models/level.py index 826684bd..0e704b2d 100644 --- a/src/c3nav/mapdata/models/level.py +++ b/src/c3nav/mapdata/models/level.py @@ -2,16 +2,26 @@ from decimal import Decimal from itertools import chain from operator import attrgetter -from django.core.validators import MinValueValidator +from django.core.validators import MinValueValidator, RegexValidator from django.db import models from django.urls import reverse from django.utils.functional import cached_property +from django.utils.regex_helper import _lazy_re_compile from django.utils.translation import gettext_lazy as _ from shapely.ops import unary_union from c3nav.mapdata.models.locations import SpecificLocation +level_index_re = _lazy_re_compile(r"^[-a-zA-Z0-9._]+\Z") +validate_level_index = RegexValidator( + level_index_re, + # Translators: "letters" means latin letters: a-z and A-Z. + _("Enter a valid “level index” consisting of letters, numbers, underscores, dots or hyphens."), + "invalid", +) + + class Level(SpecificLocation, models.Model): """ A physical level of the map, containing building, spaces, doors… @@ -26,7 +36,10 @@ class Level(SpecificLocation, models.Model): on_top_of = models.ForeignKey('mapdata.Level', null=True, on_delete=models.CASCADE, related_name='levels_on_top', verbose_name=_('on top of')) intermediate = models.BooleanField(_("intermediate level"), default=False) - short_label = models.SlugField(max_length=20, verbose_name=_('short label'), unique=True) + short_label = models.CharField(max_length=20, verbose_name=_('short label'), unique=True, + help_text=_('used for the level selector')) + level_index = models.CharField(max_length=20, verbose_name=_('level index'), unique=True, + validators=[validate_level_index], help_text=_('used for coordinates')) class Meta: verbose_name = _('Level') diff --git a/src/c3nav/mapdata/render/engines/blender.py b/src/c3nav/mapdata/render/engines/blender.py index 21e62db4..f2033435 100644 --- a/src/c3nav/mapdata/render/engines/blender.py +++ b/src/c3nav/mapdata/render/engines/blender.py @@ -265,7 +265,7 @@ class BlenderEngine(Base3DEngine): holes = geoms.holes.difference(restricted_spaces) buildings = buildings.difference(holes) - self._add_polygon('Level %s' % geoms.short_label, buildings, + self._add_polygon('Level %s' % geoms.level_index, buildings, geoms.lower_bound, geoms.upper_bound) self._set_last_polygon_to_main() diff --git a/src/c3nav/mapdata/render/geometry/level.py b/src/c3nav/mapdata/render/geometry/level.py index b9235ccd..47649f0a 100644 --- a/src/c3nav/mapdata/render/geometry/level.py +++ b/src/c3nav/mapdata/render/geometry/level.py @@ -47,6 +47,7 @@ class BaseLevelGeometries: pk: int on_top_of_id: int | None + level_index: str short_label: str base_altitude: int default_height: int @@ -360,6 +361,7 @@ class SingleLevelGeometries(BaseLevelGeometries): pk=level.pk, on_top_of_id=level.on_top_of_id, short_label=level.short_label, + level_index=level.level_index, base_altitude=base_altitude, default_height=default_height, door_height=door_height, diff --git a/src/c3nav/mapdata/render/renderdata.py b/src/c3nav/mapdata/render/renderdata.py index bec74635..e9d0dd8d 100644 --- a/src/c3nav/mapdata/render/renderdata.py +++ b/src/c3nav/mapdata/render/renderdata.py @@ -301,6 +301,7 @@ class LevelRenderData: pk=single_geoms.pk, on_top_of_id=single_geoms.on_top_of_id, short_label=single_geoms.short_label, + level_index=single_geoms.level_index, base_altitude=single_geoms.base_altitude, default_height=single_geoms.default_height, door_height=single_geoms.door_height, diff --git a/src/c3nav/mapdata/render/renderer.py b/src/c3nav/mapdata/render/renderer.py index 3a88583b..af8ee2ac 100644 --- a/src/c3nav/mapdata/render/renderer.py +++ b/src/c3nav/mapdata/render/renderer.py @@ -60,7 +60,7 @@ class MapRenderer: not_full_levels = engine.is_3d # always do non-full-levels until after the first primary level full_levels = self.full_levels and engine.is_3d for geoms in levels: - engine.add_group('level_%s' % geoms.short_label) + engine.add_group('level_%s' % geoms.level_index) if geoms.pk == level_render_data.lowest_important_level: engine.darken(level_render_data.darken_area, much=level_render_data.darken_much) diff --git a/src/c3nav/mapdata/schemas/models.py b/src/c3nav/mapdata/schemas/models.py index d261408a..f8f84443 100644 --- a/src/c3nav/mapdata/schemas/models.py +++ b/src/c3nav/mapdata/schemas/models.py @@ -32,6 +32,10 @@ class LevelSchema(SpecificLocationSchema, DjangoModelSchema): title="short label (for level selector)", description="unique among levels", ) + level_index: NonEmptyStr = APIField( + title="level index (for coordinates)", + description="unique among levels", + ) on_top_of: Union[ Annotated[PositiveInt, APIField(title="level ID", description="level this level is on top of", example=1)], Annotated[None, APIField(title="null", description="this is a main level, not on top of any other")] diff --git a/src/c3nav/mapdata/utils/cache/stats.py b/src/c3nav/mapdata/utils/cache/stats.py index f718639c..221feed7 100644 --- a/src/c3nav/mapdata/utils/cache/stats.py +++ b/src/c3nav/mapdata/utils/cache/stats.py @@ -91,7 +91,7 @@ def convert_locate(data): pos = CustomLocation(measurement.space.level, measurement.geometry.x, measurement.geometry.y, permissions=set()) space_slug = measurement.space.effective_slug - level_label = measurement.space.level.short_label + level_label = measurement.space.level.level_index grid_square = pos.grid_square if grid.enabled else None measurement_lookup[pos.pk] = (measurement.pk, grid_square, space_slug, level_label) result['by_measurement_id'][measurement.pk] = 0 @@ -138,7 +138,7 @@ def convert_location(data): # fill up lists with zeros location_slugs = {} - level_labels = {} + level_indices = {} for location in LocationSlug.objects.all(): location = location.get_child() if isinstance(location, LocationRedirect): @@ -146,9 +146,9 @@ def convert_location(data): result['locations']['by_type'].setdefault(location.__class__.__name__.lower(), {})[location.effective_slug] = 0 location_slugs[location.pk] = location.effective_slug if isinstance(location, Level): - result['locations']['by_level'][location.short_label] = 0 - result['coordinates']['by_level'][location.short_label] = 0 - level_labels[location.pk] = location.short_label + result['locations']['by_level'][location.level_index] = 0 + result['coordinates']['by_level'][location.level_index] = 0 + level_indices[location.pk] = location.short_label if isinstance(location, Space): result['locations']['by_space'][location.effective_slug] = 0 result['coordinates']['by_space'][location.effective_slug] = 0 @@ -191,7 +191,7 @@ def convert_location(data): if hasattr(location, 'space_id'): result['locations']['by_space'][location_slugs[location.space_id]] += value if hasattr(location, 'level_id'): - result['locations']['by_level'][level_labels[location.level_id]] += value + result['locations']['by_level'][level_indices[location.level_id]] += value if hasattr(location, 'groups'): for group in location.groups.all(): result['locations']['by_group'][location_slugs[group.pk]] += value diff --git a/src/c3nav/mapdata/utils/locations.py b/src/c3nav/mapdata/utils/locations.py index 31bdb59a..bea34a56 100644 --- a/src/c3nav/mapdata/utils/locations.py +++ b/src/c3nav/mapdata/utils/locations.py @@ -193,14 +193,14 @@ def locations_by_slug_for_request(request) -> Mapping[str, LocationSlug]: return locations -def levels_by_short_label_for_request(request) -> Mapping[str, Level]: - cache_key = 'mapdata:levels:by_short_label:%s' % AccessPermission.cache_key_for_request(request) +def levels_by_level_index_for_request(request) -> Mapping[str, Level]: + cache_key = 'mapdata:levels:by_level_index:%s' % AccessPermission.cache_key_for_request(request) levels = proxied_cache.get(cache_key, None) if levels is not None: return levels levels = OrderedDict( - (level.short_label, level) + (level.level_index, level) for level in Level.qs_for_request(request).filter(on_top_of_id__isnull=True).order_by('base_altitude') ) @@ -263,10 +263,10 @@ def get_location_by_slug_for_request(slug: str, request) -> Optional[Union[Locat def get_custom_location_for_request(slug: str, request): - match = re.match(r'^c:(?P[a-z0-9-_]+):(?P-?\d+(\.\d+)?):(?P-?\d+(\.\d+)?)$', slug) + match = re.match(r'^c:(?P[a-z0-9-_.]+):(?P-?\d+(\.\d+)?):(?P-?\d+(\.\d+)?)$', slug) if match is None: return None - level = levels_by_short_label_for_request(request).get(match.group('level')) + level = levels_by_level_index_for_request(request).get(match.group('level')) if not isinstance(level, Level): return None return CustomLocation(level, float(match.group('x')), float(match.group('y')), diff --git a/src/c3nav/site/converters.py b/src/c3nav/site/converters.py index cd32d26c..62f9e401 100644 --- a/src/c3nav/site/converters.py +++ b/src/c3nav/site/converters.py @@ -12,7 +12,7 @@ class LocationConverter: class CoordinatesConverter: - regex = r'[a-z0-9-_:]+:-?\d+(\.\d+)?:-?\d+(\.\d+)?' + regex = r'[a-z0-9-_.]+:-?\d+(\.\d+)?:-?\d+(\.\d+)?' def to_python(self, value): return value @@ -25,7 +25,7 @@ AtPos = namedtuple('AtPos', ('level', 'x', 'y', 'zoom')) class AtPositionConverter: - regex = r'(@[a-z0-9-_:]+,-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?)?' + regex = r'(@[a-z0-9-_.]+,-?\d+(\.\d+)?,-?\d+(\.\d+)?,-?\d+(\.\d+)?)?' def to_python(self, value): if not value: diff --git a/src/c3nav/site/static/site/js/c3nav.js b/src/c3nav/site/static/site/js/c3nav.js index 5ab9773f..05bb2635 100644 --- a/src/c3nav/site/static/site/js/c3nav.js +++ b/src/c3nav/site/static/site/js/c3nav.js @@ -758,7 +758,7 @@ c3nav = { url += 'options/' } if (state.center) { - url += '@' + String(c3nav.level_labels_by_id[state.level]) + ',' + String(state.center[0]) + ',' + String(state.center[1]) + ',' + String(state.zoom); + url += '@' + String(c3nav.level_indices_by_id[state.level]) + ',' + String(state.center[0]) + ',' + String(state.center[1]) + ',' + String(state.zoom); } return url }, @@ -1414,9 +1414,9 @@ c3nav = { c3nav.initial_level = 0 } - c3nav.level_labels_by_id = {}; + c3nav.level_indices_by_id = {}; for (i = 0; i < c3nav.levels.length; i++) { - c3nav.level_labels_by_id[c3nav.levels[i][0]] = c3nav.levels[i][1]; + c3nav.level_indices_by_id[c3nav.levels[i][0]] = c3nav.levels[i][1]; } minZoom = Math.log2(Math.max(0.25, Math.min( @@ -1468,7 +1468,7 @@ c3nav = { c3nav._labelLayer = L.LayerGroup.collision({margin: 5}).addTo(c3nav.map); for (i = c3nav.levels.length - 1; i >= 0; i--) { var level = c3nav.levels[i]; - var layerGroup = c3nav._levelControl.addLevel(level[0], level[1]); + var layerGroup = c3nav._levelControl.addLevel(level[0], level[2]); c3nav._detailLayers[level[0]] = L.layerGroup().addTo(layerGroup); c3nav._locationLayers[level[0]] = L.layerGroup().addTo(layerGroup); c3nav._routeLayers[level[0]] = L.layerGroup().addTo(layerGroup); @@ -1572,7 +1572,7 @@ c3nav = { }, _latlng_to_name: function (latlng) { var level = c3nav.current_level(); - return 'c:' + String(c3nav.level_labels_by_id[level]) + ':' + Math.round(latlng.lng * 100) / 100 + ':' + Math.round(latlng.lat * 100) / 100; + return 'c:' + String(c3nav.level_indices_by_id[level]) + ':' + Math.round(latlng.lng * 100) / 100 + ':' + Math.round(latlng.lat * 100) / 100; }, _click_anywhere_load: function (nearby, latlng) { if (!c3nav._click_anywhere_popup && !latlng) return; diff --git a/src/c3nav/site/views.py b/src/c3nav/site/views.py index 99ddae02..35fa9e01 100644 --- a/src/c3nav/site/views.py +++ b/src/c3nav/site/views.py @@ -40,7 +40,7 @@ from c3nav.mapdata.models.locations import (LocationGroup, LocationRedirect, Pos from c3nav.mapdata.models.report import Report, ReportUpdate from c3nav.mapdata.schemas.models import SlimLocationSchema from c3nav.mapdata.utils.locations import (get_location_by_id_for_request, get_location_by_slug_for_request, - levels_by_short_label_for_request) + levels_by_level_index_for_request) from c3nav.mapdata.utils.user import can_access_editor, get_user_data from c3nav.mapdata.views import set_tile_access_cookie from c3nav.routing.models import RouteOptions @@ -134,7 +134,7 @@ def map_index(request, mode=None, slug=None, slug2=None, details=None, options=N 'nearby': True if nearby else False, } - levels = levels_by_short_label_for_request(request) + levels = levels_by_level_index_for_request(request) level = levels.get(pos.level, None) if pos else None if level is not None: @@ -198,7 +198,7 @@ def map_index(request, mode=None, slug=None, slug2=None, details=None, options=N from c3nav.mapdata.models.theme import Theme ctx = { 'bounds': json.dumps(Source.max_bounds(), separators=(',', ':')), - 'levels': json.dumps(tuple((level.pk, level.short_label) for level in levels.values()), separators=(',', ':')), + 'levels': json.dumps(tuple((level.pk, level.level_index, level.short_label) for level in levels.values()), separators=(',', ':')), 'state': json.dumps(state, separators=(',', ':'), cls=DjangoJSONEncoder), 'tile_cache_server': settings.TILE_CACHE_SERVER, 'initial_level': settings.INITIAL_LEVEL,