|
| 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