diff --git a/backend/server/main/settings.py b/backend/server/main/settings.py index 5515c9b..4d9e788 100644 --- a/backend/server/main/settings.py +++ b/backend/server/main/settings.py @@ -58,6 +58,7 @@ INSTALLED_APPS = ( 'adventures', 'worldtravel', 'users', + 'django.contrib.gis', ) @@ -100,7 +101,7 @@ SIMPLE_JWT = { DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql', + 'ENGINE': 'django.contrib.gis.db.backends.postgis', 'NAME': getenv('PGDATABASE'), 'USER': getenv('PGUSER'), 'PASSWORD': getenv('PGPASSWORD'), diff --git a/backend/server/requirements.txt b/backend/server/requirements.txt index 2bd5846..41a4747 100644 --- a/backend/server/requirements.txt +++ b/backend/server/requirements.txt @@ -10,4 +10,5 @@ python-dotenv psycopg2-binary Pillow whitenoise -django-resized \ No newline at end of file +django-resized +django-geojson \ No newline at end of file diff --git a/backend/server/static/data/fr.json b/backend/server/static/data/fr.json index 405893a..8574bd4 100644 --- a/backend/server/static/data/fr.json +++ b/backend/server/static/data/fr.json @@ -4,7 +4,7 @@ { "type": "Feature", "geometry": { - "type": "Polygon", + "type": "MultiPolygon", "coordinates": [ [ [1.9221462784913, 48.457599361977], diff --git a/backend/server/static/data/mx.json b/backend/server/static/data/mx.json index b11ba3c..4342ced 100644 --- a/backend/server/static/data/mx.json +++ b/backend/server/static/data/mx.json @@ -10,7 +10,7 @@ "ISOCODE": "MX-CMX" }, "geometry": { - "type": "Polygon", + "type": "MultiPolygon", "coordinates": [ [ [-99.111241, 19.561498], diff --git a/backend/server/static/data/us.json b/backend/server/static/data/us.json index 9480fd0..557f51d 100644 --- a/backend/server/static/data/us.json +++ b/backend/server/static/data/us.json @@ -16,7 +16,7 @@ "AWATER": 23736382213 }, "geometry": { - "type": "Polygon", + "type": "MultiPolygon", "coordinates": [ [ [-94.0430515276176, 32.6930299766656], diff --git a/backend/server/worldtravel/management/commands/worldtravel-seed.py b/backend/server/worldtravel/management/commands/worldtravel-seed.py index f317bda..b63d97f 100644 --- a/backend/server/worldtravel/management/commands/worldtravel-seed.py +++ b/backend/server/worldtravel/management/commands/worldtravel-seed.py @@ -4,11 +4,66 @@ from django.contrib.auth import get_user_model import requests from worldtravel.models import Country, Region from django.db import transaction +from django.contrib.gis.geos import GEOSGeometry, Polygon, MultiPolygon +from django.contrib.gis.geos.error import GEOSException +import json from django.conf import settings media_root = settings.MEDIA_ROOT + +def setGeometry(region_code): + # Assuming the file name is the country code (e.g., 'AU.json' for Australia) + country_code = region_code.split('-')[0] + json_file = os.path.join('static/data', f'{country_code.lower()}.json') + + if not os.path.exists(json_file): + print(f'File {country_code}.json does not exist') + return None + + try: + with open(json_file, 'r') as f: + geojson_data = json.load(f) + except json.JSONDecodeError as e: + print(f"Invalid JSON in file for {country_code}: {e}") + return None + + if 'type' not in geojson_data or geojson_data['type'] != 'FeatureCollection': + print(f"Invalid GeoJSON structure for {country_code}: missing or incorrect 'type'") + return None + + if 'features' not in geojson_data or not geojson_data['features']: + print(f"Invalid GeoJSON structure for {country_code}: missing or empty 'features'") + return None + + for feature in geojson_data['features']: + try: + properties = feature.get('properties', {}) + isocode = properties.get('ISOCODE') + + if isocode == region_code: + geometry = feature['geometry'] + geos_geom = GEOSGeometry(json.dumps(geometry)) + + if isinstance(geos_geom, Polygon): + Region.objects.filter(id=region_code).update(geometry=MultiPolygon([geos_geom])) + print(f"Updated geometry for region {region_code}") + return MultiPolygon([geos_geom]) + elif isinstance(geos_geom, MultiPolygon): + Region.objects.filter(id=region_code).update(geometry=geos_geom) + print(f"Updated geometry for region {region_code}") + return geos_geom + else: + print(f"Unexpected geometry type for region {region_code}: {type(geos_geom)}") + return None + + except (KeyError, ValueError, GEOSException) as e: + print(f"Error processing region {region_code}: {e}") + + print(f"No matching region found for {region_code}") + return None + def saveCountryFlag(country_code): flags_dir = os.path.join(media_root, 'flags') @@ -616,7 +671,9 @@ class Command(BaseCommand): ) if created: self.stdout.write(f'Inserted {name} into worldtravel regions') + setGeometry(id) else: + setGeometry(id) self.stdout.write(f'Updated {name} in worldtravel regions') def insert_countries(self, countries): @@ -627,6 +684,7 @@ class Command(BaseCommand): ) if created: saveCountryFlag(country_code) + self.stdout.write(f'Inserted {name} into worldtravel countries') else: saveCountryFlag(country_code) @@ -641,5 +699,7 @@ class Command(BaseCommand): ) if created: self.stdout.write(f'Inserted {name} into worldtravel regions') + setGeometry(id) else: + setGeometry(id) self.stdout.write(f'{name} already exists in worldtravel regions') \ No newline at end of file diff --git a/backend/server/worldtravel/migrations/0004_country_geometry.py b/backend/server/worldtravel/migrations/0004_country_geometry.py new file mode 100644 index 0000000..89c8f2d --- /dev/null +++ b/backend/server/worldtravel/migrations/0004_country_geometry.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.8 on 2024-08-23 17:01 + +import django.contrib.gis.db.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('worldtravel', '0003_alter_region_name_en'), + ] + + operations = [ + migrations.AddField( + model_name='country', + name='geometry', + field=django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326), + ), + ] diff --git a/backend/server/worldtravel/migrations/0005_remove_country_geometry_region_geometry.py b/backend/server/worldtravel/migrations/0005_remove_country_geometry_region_geometry.py new file mode 100644 index 0000000..7a32c71 --- /dev/null +++ b/backend/server/worldtravel/migrations/0005_remove_country_geometry_region_geometry.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.8 on 2024-08-23 17:47 + +import django.contrib.gis.db.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('worldtravel', '0004_country_geometry'), + ] + + operations = [ + migrations.RemoveField( + model_name='country', + name='geometry', + ), + migrations.AddField( + model_name='region', + name='geometry', + field=django.contrib.gis.db.models.fields.MultiPolygonField(blank=True, null=True, srid=4326), + ), + ] diff --git a/backend/server/worldtravel/models.py b/backend/server/worldtravel/models.py index 8f4aca0..de0d87f 100644 --- a/backend/server/worldtravel/models.py +++ b/backend/server/worldtravel/models.py @@ -1,6 +1,7 @@ from django.db import models from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError +from django.contrib.gis.db import models as gis_models User = get_user_model() @@ -34,6 +35,7 @@ class Country(models.Model): choices=CONTINENT_CHOICES, default=AFRICA ) + class Meta: verbose_name = "Country" @@ -47,6 +49,7 @@ class Region(models.Model): name = models.CharField(max_length=100) name_en = models.CharField(max_length=100, blank=True, null=True) country = models.ForeignKey(Country, on_delete=models.CASCADE) + geometry = gis_models.MultiPolygonField(srid=4326, null=True, blank=True) def __str__(self): return self.name diff --git a/backend/server/worldtravel/serializers.py b/backend/server/worldtravel/serializers.py index 051a9b1..a41ae70 100644 --- a/backend/server/worldtravel/serializers.py +++ b/backend/server/worldtravel/serializers.py @@ -16,7 +16,7 @@ class CountrySerializer(serializers.ModelSerializer): class Meta: model = Country fields = '__all__' # Serialize all fields of the Adventure model - read_only_fields = ['id', 'name', 'country_code', 'continent', 'flag_url'] + read_only_fields = ['id', 'name', 'country_code', 'continent', 'flag_url', 'geometry'] class RegionSerializer(serializers.ModelSerializer): class Meta: diff --git a/backend/server/worldtravel/views.py b/backend/server/worldtravel/views.py index 1db5674..3bac696 100644 --- a/backend/server/worldtravel/views.py +++ b/backend/server/worldtravel/views.py @@ -8,7 +8,10 @@ from rest_framework.response import Response from rest_framework.decorators import api_view, permission_classes import os import json +from django.http import JsonResponse +from django.contrib.gis.geos import Point from django.conf import settings +from rest_framework.decorators import action from django.contrib.staticfiles import finders @api_view(['GET']) @@ -34,6 +37,19 @@ class CountryViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = CountrySerializer permission_classes = [IsAuthenticated] + @action(detail=False, methods=['get']) + def check_point_in_region(self, request): + lat = float(request.query_params.get('lat')) + lon = float(request.query_params.get('lon')) + point = Point(lon, lat, srid=4326) + + region = Region.objects.filter(geometry__contains=point).first() + + if region: + return Response({'in_region': True, 'region_name': region.name}) + else: + return Response({'in_region': False}) + class RegionViewSet(viewsets.ReadOnlyModelViewSet): queryset = Region.objects.all() serializer_class = RegionSerializer