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 django.core.management.base import BaseCommand
|
||||||
from pydantic import Field, BaseModel
|
from pydantic import Field, BaseModel
|
||||||
from pydantic import PositiveInt
|
from pydantic import PositiveInt
|
||||||
|
from shapely import Point
|
||||||
|
from shapely.geometry import shape
|
||||||
|
|
||||||
from c3nav.api.utils import NonEmptyStr
|
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):
|
class HubImportItem(BaseModel):
|
||||||
|
@ -22,8 +27,8 @@ class HubImportItem(BaseModel):
|
||||||
public_url: NonEmptyStr = Field(pattern=r'^https://')
|
public_url: NonEmptyStr = Field(pattern=r'^https://')
|
||||||
parent_id: UUID | None
|
parent_id: UUID | None
|
||||||
children: list[NonEmptyStr] | None
|
children: list[NonEmptyStr] | None
|
||||||
floor: PositiveInt
|
floor: PositiveInt | None
|
||||||
location: tuple[float, float]
|
location: tuple[float, float] | None
|
||||||
polygons: tuple[list[tuple[float, float]]] | None
|
polygons: tuple[list[tuple[float, float]]] | None
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,131 +36,174 @@ class Command(BaseCommand):
|
||||||
help = 'import from hub'
|
help = 'import from hub'
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
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})
|
headers={"Authorization": "Token "+settings.HUB_API_SECRET})
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
from pprint import pprint
|
|
||||||
|
|
||||||
data = [
|
with MapUpdate.lock():
|
||||||
{
|
changed_geometries.reset()
|
||||||
"type": "assembly",
|
self.do_import(r.json())
|
||||||
"id": "037c0b9c-6283-42e4-9c43-24e072e8718e",
|
MapUpdate.objects.create(type='importhub')
|
||||||
"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
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
|
def do_import(self, data):
|
||||||
items: list[HubImportItem] = [HubImportItem.model_validate(item) for item in data]
|
items: list[HubImportItem] = [HubImportItem.model_validate(item) for item in data]
|
||||||
items_by_id = {item.id: item for item in items}
|
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:
|
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 = [
|
hub_types = [
|
||||||
item.type,
|
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
|
geometry: BaseGeometry
|
||||||
level_id: int
|
level_id: int
|
||||||
subtitle: str
|
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:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue