add level_index so short_label can look nicer

This commit is contained in:
Laura Klünder 2024-12-19 10:56:38 +01:00
parent 6167c92e98
commit fe98f5e618
14 changed files with 89 additions and 31 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<level>[a-z0-9-_]+):(?P<x>-?\d+(\.\d+)?):(?P<y>-?\d+(\.\d+)?)$', slug)
match = re.match(r'^c:(?P<level>[a-z0-9-_.]+):(?P<x>-?\d+(\.\d+)?):(?P<y>-?\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')),

View file

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

View file

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

View file

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