Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/export geojson #12

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ venv.bak/

# Node
node_modules/
frontend/src/environments/version.ts
frontend/src/environments/version.ts

# IDEs
.vscode/
.idea/
2 changes: 2 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,5 @@ venv/
ENV/
env.bak/
venv.bak/

external_data/
5 changes: 5 additions & 0 deletions backend/data/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import tempfile

from django.contrib import admin
from django import forms
from django.http import HttpResponse

from django_admin_search.admin import AdvancedSearchAdmin

from .geojson import export_geojson
from .models import (
Area, Region, Place, Record, PrimaryCategory, SecondaryCategory,
Language, Script, Century
Expand Down Expand Up @@ -43,6 +47,7 @@ def fetch_from_pleiades(modeladmin, request, queryset):
item.fetch_from_pleiades()
item.save()


@admin.register(Place)
class PlaceAdmin(admin.ModelAdmin):
actions = [fetch_from_pleiades]
Expand Down
82 changes: 82 additions & 0 deletions backend/data/geojson.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Export to GeoJSON support."""
import json
from dataclasses import dataclass, field
from pathlib import Path

from typing import Dict, List, Union, Optional

import data.models as models
from .serializers import RecordSerializer

Feature = Dict[str, any]


@dataclass
class FeatureData:
area: str
province_region: str
placename: str
latitude: Optional[float] = None
longitude: Optional[float] = None
pleiades: Optional[int] = None
record_data: List[Dict] = field(default_factory=list)

def to_geojson_feature(self, number_of_inscriptions: Union[int, str]) -> \
Optional[Feature]:
feature: Feature = {
'type': 'Feature',
'geometry': {
'type': 'Point',
'coordinates': [
self.longitude,
self.latitude
] # GeoJSON uses this order but it is different to daily use
},
'properties': {
'placename': self.placename,
'Province_region': self.province_region,
'area': self.area,
'inscriptions-count': number_of_inscriptions,
'records': self.record_data,
}
}
if self.pleiades:
feature['properties']['pleiades'] = self.pleiades
return feature


def create_geojson() -> Dict:
"""Export to GeoJSON as a Python dictionary."""
places = models.Place.objects.prefetch_related("records").all()
features: List[Feature] = []
# Go through list of places to get one feature per place
for place in places:
assert isinstance(place, models.Place)
records: List[models.Record] = list(place.records.all())
if len(records) == 0:
# We do not want to plot places on the map for which there are no records.
continue
area = place.area.name if place.area else ""
province_region = place.region.name if place.region else ""
fd = FeatureData(area=area, province_region=province_region, placename=place.name,
pleiades=place.pleiades_id)
if place.coordinates:
fd.longitude, fd.latitude = place.coordinates.coords # Point stores first x, then y
record_data: List[Dict] = []
number_of_inscriptions = 0
for record in records:
number_of_inscriptions += record.inscriptions_count
record_data.append(RecordSerializer(record).data)
fd.record_data = record_data
features.append(fd.to_geojson_feature(number_of_inscriptions))

geojson: Dict[str, Union[str, List[Feature]]] = {
"type": "FeatureCollection",
"features": features,
}
return geojson


def export_geojson(filepath: Path, ) -> None:
with open(filepath, 'w') as f:
json.dump(create_geojson(), f, indent=4)
23 changes: 23 additions & 0 deletions backend/data/management/commands/export_json.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from pathlib import Path
from typing import Optional

from django.core.management import BaseCommand

from data.geojson import export_geojson

class Command(BaseCommand):
help = '''
Export all records to GeoJSON file to plot on a map.
'''
def add_arguments(self, parser):
parser.add_argument(
'--export_path',
help='''Path to JSON file to write to''',
default=None
)

def handle(self, export_path: Optional[str], **options):
if export_path is None:
export_path = Path.cwd() / "features.geojson"
export_geojson(export_path)
self.stdout.write(self.style.SUCCESS(f'Features written to {export_path}.'))
7 changes: 7 additions & 0 deletions backend/data/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import path

from .views import download_geojson

urlpatterns = [
path('geojson.json', download_geojson)
]
15 changes: 15 additions & 0 deletions backend/data/views.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import json

from django.http import HttpResponse, HttpRequest
from rest_framework import viewsets
from rest_framework import permissions

from .geojson import create_geojson
from .models import Record
from .serializers import RecordSerializer

Expand All @@ -12,3 +17,13 @@ class RecordViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = RecordSerializer
# permission_classes = [permissions.IsAuthenticated]


def download_geojson(request: HttpRequest) -> HttpResponse:
"""Download a GeoJSON file containing all existing records."""
geojson_dict = create_geojson()
response = HttpResponse(
content_type="application/geo+json"
)
response.content = json.dumps(geojson_dict, indent=4).encode()
return response

1 change: 1 addition & 0 deletions backend/jhm/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@
'rest_framework.urls',
namespace='rest_framework',
)),
path('data/', include('data.urls')),
spa_url, # catch-all; unknown paths to be handled by a SPA
]
11 changes: 5 additions & 6 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
#
--no-binary psycopg2

asgiref==3.7.2
asgiref==3.8.1
# via django
backports-zoneinfo==0.2.1 ; python_version < "3.9"
# via
# -r requirements.in
# django
# djangorestframework
certifi==2024.2.2
# via requests
charset-normalizer==3.3.2
Expand All @@ -29,7 +30,7 @@ django-livereload-server==0.5.1
# via -r requirements.in
django-revproxy==0.12.0
# via -r requirements.in
djangorestframework==3.14.0
djangorestframework==3.15.1
# via -r requirements.in
et-xmlfile==1.1.0
# via openpyxl
Expand All @@ -45,13 +46,13 @@ iniconfig==2.0.0
# via pytest
openpyxl==3.1.2
# via -r requirements.in
packaging==23.2
packaging==24.0
# via pytest
pluggy==1.4.0
# via pytest
psycopg2==2.9.9
# via -r requirements.in
pytest==8.0.2
pytest==8.1.1
# via
# -r requirements.in
# pytest-django
Expand All @@ -60,8 +61,6 @@ pytest-django==4.8.0
# via -r requirements.in
pytest-xdist==3.5.0
# via -r requirements.in
pytz==2024.1
# via djangorestframework
requests==2.31.0
# via -r requirements.in
sqlparse==0.4.4
Expand Down