add importhub first draft
This commit is contained in:
parent
a4b72478fa
commit
04f7c27ecb
4 changed files with 275 additions and 120 deletions
|
@ -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")
|
||||
|
||||
|
|
14
src/c3nav/mapdata/migrations/0096_merge_20231225_2216.py
Normal file
14
src/c3nav/mapdata/migrations/0096_merge_20231225_2216.py
Normal 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 = [
|
||||
]
|
93
src/c3nav/mapdata/migrations/0097_longer_import_tag.py
Normal file
93
src/c3nav/mapdata/migrations/0097_longer_import_tag.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue