Skip to content

Commit 0c504b9

Browse files
Merge pull request #1521 from developmentseed/admin2_import
Script to import admin2
2 parents 0515291 + ec1b4b4 commit 0c504b9

File tree

2 files changed

+215
-0
lines changed

2 files changed

+215
-0
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from django.core.management.base import BaseCommand, CommandError
2+
from django.db import transaction
3+
import geopandas as gpd
4+
5+
6+
class Command(BaseCommand):
7+
help = "Converting Shapefiles from different sources to custom file for go-api. To run, python manage.py generate-admin2-shp input.shp output.shp --source=fews"
8+
9+
missing_args_message = "Filename is missing. A shapefile with valid admin polygons is required."
10+
11+
def add_arguments(self, parser):
12+
parser.add_argument("filename", nargs="+", type=str)
13+
parser.add_argument("--source", help="Shapefile source, Can be : gadm or fews")
14+
15+
@transaction.atomic
16+
def handle(self, *args, **options):
17+
input_filename = options["filename"][0]
18+
output_filename = options["filename"][1]
19+
try:
20+
gdf = gpd.read_file(input_filename)
21+
except:
22+
raise CommandError("Could not open file")
23+
if options["source"] == "fews":
24+
gdf.rename(columns={"shapeName": "name"}, inplace=True)
25+
gdf.rename(columns={"shapeID": "code"}, inplace=True)
26+
if options["source"] == "gadm":
27+
gdf.rename(columns={"NAME_2": "name"}, inplace=True)
28+
gdf.rename(columns={"GID_2": "code"}, inplace=True)
29+
gdf.to_file(output_filename)
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import csv
2+
from django.core.management.base import BaseCommand, CommandError
3+
from django.contrib.gis.gdal import DataSource
4+
from django.contrib.gis.geos import GEOSGeometry
5+
from django.contrib.gis.geos import MultiPolygon
6+
from django.core.exceptions import ObjectDoesNotExist
7+
from django.db import IntegrityError
8+
from django.db import transaction
9+
from api.models import Country
10+
from api.models import District
11+
from api.models import DistrictGeoms
12+
from api.models import Admin2
13+
from api.models import Admin2Geoms
14+
15+
class Command(BaseCommand):
16+
help = "import a shapefile of administrative boundary level 2 data to the GO database. To run, python manage.py import-admin2-data input.shp --country-iso2=af"
17+
18+
missing_args_message = "Filename is missing. A shapefile with valid admin polygons is required."
19+
20+
def add_arguments(self, parser):
21+
parser.add_argument('filename', nargs='+', type=str)
22+
parser.add_argument(
23+
'--update-geom',
24+
action='store_true',
25+
help='Update the geometry of the admin2.'
26+
)
27+
parser.add_argument(
28+
'--update-bbox',
29+
action='store_true',
30+
help='Update the bbox of the admin2 geometry. Used if you want to overwrite changes that are made by users via the Django Admin'
31+
)
32+
parser.add_argument(
33+
'--update-centroid',
34+
action='store_true',
35+
help='Update the centroid of the admin2 geometry. Used if you want to overwrite changes that are made by users via the Django Admin'
36+
)
37+
parser.add_argument(
38+
'--import-missing',
39+
help='Import missing admin2 boundaries for codes mentioned in this file.'
40+
)
41+
parser.add_argument(
42+
'--import-all',
43+
action='store_true',
44+
help='Import all admin2 boundaries in the shapefile, if possible.'
45+
)
46+
parser.add_argument(
47+
'--country-iso2',
48+
type=str,
49+
required=True,
50+
help='Country iso2 code'
51+
)
52+
53+
@transaction.atomic
54+
def handle(self, *args, **options):
55+
filename = options["filename"][0]
56+
# a dict to hold all the admin2 that needs to be manually imported
57+
import_missing = {}
58+
if options["import_missing"]:
59+
import_file = csv.DictReader(open(options["import_missing"]), fieldnames=["code", "name"])
60+
next(import_file)
61+
for row in import_file:
62+
code = row["code"]
63+
name = row["name"]
64+
import_missing[code] = {"code": code, "name": name}
65+
print("will import these codes", import_missing.keys())
66+
else:
67+
# if no filename is specified, open one to write missing code and names
68+
missing_filename = "missing-admin2.txt"
69+
print(f"will write missing admin2 codes to {missing_filename}")
70+
missing_file = csv.DictWriter(open(missing_filename, "w"), fieldnames=["code", "name"])
71+
missing_file.writeheader()
72+
73+
try:
74+
data = DataSource(filename)
75+
except:
76+
raise CommandError("Could not open file")
77+
78+
# loop through each feature in the shapefile
79+
for feature in data[0]:
80+
code = feature.get("code")
81+
name = feature.get("name")
82+
geom_wkt = feature.geom.wkt
83+
geom = GEOSGeometry(geom_wkt, srid=4326)
84+
if geom.geom_type == "Polygon":
85+
geom = MultiPolygon(geom)
86+
87+
centroid = geom.centroid.wkt
88+
bbox = geom.envelope.wkt
89+
# import all shapes for admin2
90+
if options["import_all"]:
91+
self.add_admin2(options, "all", feature, geom, centroid, bbox)
92+
else:
93+
admin2_objects = Admin2.objects.filter(code=code)
94+
if len(admin2_objects) == 0:
95+
if options["import_missing"]:
96+
# if it doesn't exist, add it
97+
self.add_admin2(options, import_missing, feature, geom, centroid, bbox)
98+
else:
99+
missing_file.writerow({"code": code, "name": name})
100+
101+
# if there are more than one admin2 with the same code, filter also using name
102+
if len(admin2_objects) > 1:
103+
admins2_names = Admin2.objects.filter(code=code, name__icontains=name)
104+
# if we get a match, update geometry. otherwise consider this as missing because it's possible the names aren't matching.
105+
if len(admins2_names):
106+
# update geom, centroid and bbox
107+
self.update_admin2_columns(options, admins2_names[0], geom, centroid, bbox)
108+
else:
109+
if options["import_missing"]:
110+
# if it doesn't exist, add it
111+
self.add_admin2(options, import_missing, feature, geom, centroid, bbox)
112+
else:
113+
missing_file.writerow({"code": code, "name": name})
114+
if len(admin2_objects) == 1:
115+
self.update_admin2_columns(options, admin2_objects[0], geom, centroid, bbox)
116+
print("done!")
117+
118+
@transaction.atomic
119+
def add_admin2(self, options, import_missing, feature, geom, centroid, bbox):
120+
code = feature.get("code") or "N.A"
121+
name = feature.get("name")
122+
admin2 = Admin2()
123+
admin2.code = code
124+
admin2.name = name
125+
admin2.centroid = centroid
126+
admin2.bbox = bbox
127+
country_iso2 = options["country_iso2"]
128+
# find district_id based on centroid of admin2 and country.
129+
try:
130+
admin2.admin1_id = self.find_district_id(centroid, country_iso2)
131+
except ObjectDoesNotExist:
132+
print(f"Country({country_iso2}) or admin 1 does not found for - admin2: {name}")
133+
pass
134+
135+
# save data
136+
if admin2.admin1_id is not None and ((import_missing == "all") or (code in import_missing.keys())):
137+
try:
138+
admin2.save()
139+
print("importing", admin2.name)
140+
if options["update_geom"]:
141+
self.update_geom(admin2, geom)
142+
except IntegrityError as e:
143+
print(f"Duplicate object {admin2.name}")
144+
pass
145+
146+
def update_geom(self, admin2, geom):
147+
try:
148+
Admin2Geom = Admin2Geoms.objects.get(admin2=admin2)
149+
Admin2Geom.geom = geom
150+
Admin2Geom.save()
151+
except ObjectDoesNotExist:
152+
Admin2Geom = Admin2Geoms()
153+
Admin2Geom.admin2 = admin2
154+
Admin2Geom.geom = geom
155+
Admin2Geom.save()
156+
157+
def find_district_id(self, centroid, country_iso2):
158+
"""Find district_id for admin2, according to the point with in the district polygon.
159+
Args:
160+
centroid (str): Admin2 centroid
161+
country_iso2 (str): Country iso2
162+
"""
163+
admin1_id = None
164+
country_id = Country.objects.get(iso=country_iso2)
165+
if country_id is not None:
166+
districts = District.objects.filter(country_id=country_id)
167+
districts_ids = [d.id for d in districts]
168+
districts_geoms = DistrictGeoms.objects.filter(district_id__in=districts_ids)
169+
centroid_geom = GEOSGeometry(centroid, srid=4326)
170+
for district_geom in districts_geoms:
171+
if centroid_geom.within(district_geom.geom):
172+
admin1_id = district_geom.district_id
173+
break
174+
return admin1_id
175+
176+
def update_admin2_columns(self, options, admin2, geom, centroid, bbox):
177+
if options["update_geom"]:
178+
print(f"Update geom for {admin2.name}")
179+
self.update_geom(admin2, geom)
180+
if options["update_centroid"]:
181+
print(f"Update centroid for {admin2.name}")
182+
admin2.centroid = centroid
183+
if options["update_bbox"]:
184+
print(f"Update bbox for {admin2.name}")
185+
admin2.bbox = bbox
186+
admin2.save()

0 commit comments

Comments
 (0)