add importhub first draft

This commit is contained in:
Laura Klünder 2023-12-25 22:48:54 +01:00
parent a4b72478fa
commit 04f7c27ecb
4 changed files with 275 additions and 120 deletions

View file

@ -5,8 +5,13 @@ from django.conf import settings
from django.core.management.base import BaseCommand
from pydantic import Field, BaseModel
from pydantic import PositiveInt
from shapely import Point
from shapely.geometry import shape
from c3nav.api.utils import NonEmptyStr
from c3nav.mapdata.models import Area, Space, LocationGroup, LocationSlug, MapUpdate
from c3nav.mapdata.models.geometry.space import POI
from c3nav.mapdata.utils.cache.changes import changed_geometries
class HubImportItem(BaseModel):
@ -22,8 +27,8 @@ class HubImportItem(BaseModel):
public_url: NonEmptyStr = Field(pattern=r'^https://')
parent_id: UUID | None
children: list[NonEmptyStr] | None
floor: PositiveInt
location: tuple[float, float]
floor: PositiveInt | None
location: tuple[float, float] | None
polygons: tuple[list[tuple[float, float]]] | None
@ -31,131 +36,174 @@ class Command(BaseCommand):
help = 'import from hub'
def handle(self, *args, **options):
r = requests.get(settings.HUB_API_BASE+"/integration/c3nav",
r = requests.get(settings.HUB_API_BASE+"/integration/c3nav?all=1",
headers={"Authorization": "Token "+settings.HUB_API_SECRET})
r.raise_for_status()
from pprint import pprint
data = [
{
"type": "assembly",
"id": "037c0b9c-6283-42e4-9c43-24e072e8718e",
"slug": "fnord",
"name": "Fnord Example Habitat",
"is_official": False,
"description": {
"de": "Beispiel Habitat",
"en": "Example Cluster"
},
"public_url": "https://hub.example.net/assembly/fnord/",
"parent_id": None,
"children": ["child"],
"floor": 2,
"location": [
13.301537039641516,
53.03217295491487
],
"polygons": [
[
[
13.307995798949293,
53.03178583543769
],
[
13.30780267990096,
53.030276036273506
],
[
13.310034277800554,
53.03009537300366
],
[
13.310205939178047,
53.0315793702961
],
[
13.307995798949293,
53.03178583543769
]
]
]
},
{
"type": "assembly",
"id": "085657cc-9b46-4a71-853c-70e10a371e57",
"slug": "kika",
"name": "Kinderkanal",
"is_official": True,
"description": {
"de": "Danke für deinen Betrag.",
"en": "Thanks for your support."
},
"public_url": "https://hub.example.net/assembly/kika/",
"parent_id": None,
"children": None,
"floor": 62,
"location": [
13.300807478789807,
53.032327801732634
],
"polygons": None
},
{
"type": "assembly",
"id": "fa1f2c57-fd54-47e6-add4-e483654b6741",
"slug": "child",
"name": "Assembly des Habitats #23",
"is_official": False,
"description": {
"de": None,
"en": "Sometimes, an example is all you get."
},
"public_url": "https://hub.example.net/assembly/child/",
"parent_id": "037c0b9c-6283-42e4-9c43-24e072e8718e",
"children": None,
"floor": 2,
"location": [
12.997614446551779,
53.040472311905404
],
"polygons": [
[
[
13.308124544982434,
53.03139871248601
],
[
13.308231833342973,
53.03173421924521
],
[
13.309240343932686,
53.03166969891683
],
[
13.309197428588305,
53.031218053919105
],
[
13.308146002654183,
53.03123095812731
],
[
13.308124544982434,
53.03139871248601
]
]
]
}
]
with MapUpdate.lock():
changed_geometries.reset()
self.do_import(r.json())
MapUpdate.objects.create(type='importhub')
def do_import(self, data):
items: list[HubImportItem] = [HubImportItem.model_validate(item) for item in data]
items_by_id = {item.id: item for item in items}
spaces_for_level = {}
for space in Space.objects.all():
spaces_for_level.setdefault(space.level_id, []).append(space)
locations_so_far = {
**{poi.import_tag: poi for poi in POI.objects.filter(import_tag__startswith="hub:")},
**{area.import_tag: area for area in Area.objects.filter(import_tag__startswith="hub:")},
}
groups_for_types = {
group.hub_import_type: group
for group in LocationGroup.objects.filter(hub_import_type__isnull=False)
}
for item in items:
if item.polygons is None and item.location is None:
print(f"SKIPPING: {item.slug} / {item.id} has no polygon or location")
continue
if item.floor is None:
print(f"SKIPPING: {item.slug} / {item.id} has no floor")
continue
import_tag = f"hub:{item.id}"
# determine geometry
target_type = Area if item.polygons else POI
if target_type == Area:
new_geometry = shape({
"type": "Polygon",
"coordinates": [[[y, x] for x, y in item.polygons[0]]],
})
elif target_type == POI:
new_geometry = shape({
"type": "Point",
"coordinates": list(reversed(item.location)),
})
else:
raise ValueError
# determine space
try:
possible_spaces = [space for space in spaces_for_level[item.floor]
if space.geometry.intersects(new_geometry)]
except KeyError:
print(f"ERROR: {item.slug} / {item.id} has invalid level ID")
continue
if not possible_spaces:
print(f"ERROR: {item.slug} / {item.id} is not within any space")
continue
if len(possible_spaces) == 1:
new_space = possible_spaces[0]
elif target_type == Area:
new_space = max(possible_spaces, key=lambda s: s.geometry.intersection(new_geometry).area)
elif target_type == POI:
print(f"WARNING: {item.slug} / {item.id} could be in multiple spaces, picking one...")
new_space = possible_spaces[0]
else:
raise ValueError
# find existing location
result = locations_so_far.pop(import_tag, None)
result: Area | POI | None
if result is not None:
# already exists
if not isinstance(result, target_type):
# need to change from POI to Area or inverted
if result.import_block_geom:
print(f"ERROR: {item.slug} / {item.id} needs to be switched to {target_type} but is blocked")
continue
print(f"ERROR: {item.slug} / {item.id} needs to be switched to {target_type} but not implemented")
continue
hub_types = [
item.type,
"%s:%s" % (item.type, f"parent:{items_by_id[item.parent_id].slug}" if item.parent_id else "no-parent")
"%s:%s" % (item.type, "with-children" if item.children else "with-no-children"),
"%s:%s" % (item.type, f"parent:{items_by_id[item.parent_id].slug}" if item.parent_id else "no-parent"),
]
print(hub_types)
pprint(r.json())
# build groups
new_groups = [group for hub_type, group in groups_for_types.items() if hub_type in hub_types]
# build resulting object
is_new = False
if not result:
is_new = True
result = target_type(
import_tag=import_tag,
)
if result.space_id != new_space.pk:
if result.import_block_geom:
print(f"NOTE: {item.slug} / {item.id} space has changed but is blocked")
else:
result.space_id = new_space.pk
if result.geometry != new_geometry or True:
if result.import_block_geom:
print(f"NOTE: {item.slug} / {item.id} geometry has changed but is blocked")
else:
result.geometry = new_geometry
if target_type == Area:
new_main_point = Point(item.location) if item.location else None
if result.main_point != new_main_point:
if result.import_block_geom:
print(f"NOTE: {item.slug} / {item.id} main point has changed but is blocked")
else:
result.main_point = new_main_point
if item.slug != result.slug:
if result.import_block_data:
print(f"NOTE: {item.slug} / {item.id} slug has changed but is blocked")
else:
if LocationSlug.objects.filter(slug=item.slug):
print(f"ERROR: {item.slug} / {item.id} slug {item.slug!r} is already occupied")
continue
else:
result.slug = item.slug
new_titles = {"en": item.name}
if new_titles != result.titles:
if result.import_block_data:
print(f"NOTE: {item.slug} / {item.id} name has changed but is blocked")
else:
result.titles = new_titles
if item.public_url != result.external_url:
if result.import_block_data:
print(f"NOTE: {item.slug} / {item.id} external url has changed but is blocked")
else:
result.external_url = item.public_url
# time to check the groups
new_group_ids = set(group.id for group in new_groups)
if is_new:
if not new_group_ids:
print(f"SKIPPING: {item.slug} / {item.id} has no group IDs, {hub_types}")
continue
else:
if not new_group_ids:
print(f"SKIPPING: {item.slug} / {item.id} no longer has any group IDs, {hub_types}")
continue
result.save()
if is_new:
result.groups.set(new_group_ids)
else:
old_group_ids = set(group.pk for group in result.groups.all())
if new_group_ids != old_group_ids:
print(f"NOTE: {item.slug} / {item.id} groups have changed, was " +
str([group.title for group in result.groups.all()]) +
", is now"+str([group.title for group in new_groups]), new_group_ids, old_group_ids)
for import_tag, location in locations_so_far.items():
print(f"NOTE: {location.slug} / {import_tag} should be deleted")

View file

@ -0,0 +1,14 @@
# Generated by Django 4.2.7 on 2023-12-25 21:16
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('mapdata', '0095_accesspermission_for_session'),
('mapdata', '0095_import_block'),
]
operations = [
]

View file

@ -0,0 +1,93 @@
# Generated by Django 4.2.7 on 2023-12-25 21:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mapdata', '0096_merge_20231225_2216'),
]
operations = [
migrations.AlterField(
model_name='altitudearea',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='altitudemarker',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='area',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='building',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='column',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='door',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='graphnode',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='hole',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='lineobstacle',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='obstacle',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='poi',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='ramp',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='rangingbeacon',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='space',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='stair',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
migrations.AlterField(
model_name='wifimeasurement',
name='import_tag',
field=models.CharField(blank=True, max_length=64, null=True, verbose_name='import tag'),
),
]

View file

@ -24,7 +24,7 @@ class GeometryMixin(SerializableMixin):
geometry: BaseGeometry
level_id: int
subtitle: str
import_tag = models.CharField(_('import tag'), null=True, blank=True, max_length=32)
import_tag = models.CharField(_('import tag'), null=True, blank=True, max_length=64)
class Meta:
abstract = True