2021-10-22 16:01:31 +13:00
|
|
|
import json
|
|
|
|
import os
|
2023-02-08 14:16:07 +13:00
|
|
|
import re
|
2021-10-22 16:01:31 +13:00
|
|
|
import requests
|
|
|
|
from urllib.parse import urlencode
|
2023-02-08 14:16:07 +13:00
|
|
|
from unicodedata import normalize
|
2021-10-22 16:01:31 +13:00
|
|
|
|
2023-02-21 16:43:46 +13:00
|
|
|
from django.db.models import Q
|
2021-10-22 16:01:31 +13:00
|
|
|
from django.contrib.gis.geos import Point, GEOSGeometry
|
2023-02-21 16:43:46 +13:00
|
|
|
|
|
|
|
from .models import Address
|
|
|
|
|
2021-10-22 16:01:31 +13:00
|
|
|
|
|
|
|
LINZ_API_KEY = os.getenv("LINZ_API_KEY")
|
|
|
|
LINZ_WFS_ENDPOINT = f"https://data.linz.govt.nz/services;key={LINZ_API_KEY}/wfs"
|
|
|
|
PROPERTY_TILE_LAYER = "layer-50804"
|
|
|
|
PROPERTY_INFO_LAYER = "layer-53353"
|
|
|
|
|
|
|
|
|
2023-02-08 14:16:07 +13:00
|
|
|
search_num = re.compile(r'((?<!\S)\d+)')
|
|
|
|
search_str = re.compile(r'((?<!\S)[a-zA-Z\-]+)')
|
|
|
|
|
|
|
|
|
2021-10-22 16:01:31 +13:00
|
|
|
class WFSError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def get_point_from_coordinates(coordinates):
|
|
|
|
"""Given a coordinates json string, returns the coordinates as a Point object"""
|
2023-02-21 16:43:46 +13:00
|
|
|
if isinstance(coordinates, Point):
|
|
|
|
return coordinates
|
|
|
|
elif isinstance(coordinates, str):
|
|
|
|
coordinates_json = json.loads(coordinates)
|
|
|
|
elif isinstance(coordinates, dict):
|
|
|
|
coordinates_json = coordinates
|
|
|
|
|
|
|
|
return Point(coordinates_json["lng"], coordinates_json["lat"], srid=4326)
|
2021-10-22 16:01:31 +13:00
|
|
|
|
|
|
|
|
|
|
|
def wfs_getfeature(endpoint, **kwargs):
|
|
|
|
"""Perform a WFS request with the with all keyword arguments as parameters
|
|
|
|
within the URL query, returning the JSON-formatted response"""
|
|
|
|
params = {
|
|
|
|
'service': "WFS",
|
|
|
|
'request': "GetFeature",
|
|
|
|
'outputFormat': "application/json",
|
|
|
|
**kwargs,
|
|
|
|
}
|
|
|
|
|
|
|
|
query = urlencode(params)
|
|
|
|
url = f"{endpoint}?{query}"
|
|
|
|
response = requests.get(url)
|
|
|
|
|
|
|
|
try:
|
|
|
|
return response.json()
|
2023-04-20 15:12:22 +12:00
|
|
|
except json.JSONDecodeError:
|
2021-10-22 16:01:31 +13:00
|
|
|
raise WFSError(
|
|
|
|
f"Failed to make WFS request to {url}: {response.content}")
|
|
|
|
|
|
|
|
|
|
|
|
def linz_wfs_getfeature(**kwargs):
|
|
|
|
"""Perform a WFS request via the LINZ endpoint"""
|
|
|
|
return wfs_getfeature(LINZ_WFS_ENDPOINT, version="2.0.0", **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
def intersecting_property(coordinates):
|
|
|
|
"""Finds the property that intersects with the coordinates and returns its definition as a dictionary"""
|
|
|
|
point = get_point_from_coordinates(coordinates)
|
|
|
|
cql_filter = f"Intersects(shape,{point})"
|
|
|
|
json = linz_wfs_getfeature(
|
|
|
|
typeNames=PROPERTY_TILE_LAYER, cql_filter=cql_filter, count=1)
|
|
|
|
features = json.get("features")
|
|
|
|
|
|
|
|
if len(features) > 0:
|
|
|
|
return features[0]
|
|
|
|
|
|
|
|
|
|
|
|
def get_address(polygon):
|
|
|
|
"""Fetch the address string that intersects with the specified polygon"""
|
|
|
|
|
|
|
|
# Don't perform request if the input polygon WKT is too long to fit into URL
|
|
|
|
# Limit is 2048 characters
|
|
|
|
if len(str(polygon)) > 1500:
|
|
|
|
return None
|
|
|
|
|
|
|
|
cql_filter = f"Within(shape,{polygon})"
|
|
|
|
json = linz_wfs_getfeature(
|
|
|
|
typeNames=PROPERTY_INFO_LAYER, cql_filter=cql_filter, count=1)
|
|
|
|
features = json.get("features")
|
|
|
|
|
|
|
|
if len(features) > 0:
|
|
|
|
feature = features[0]
|
|
|
|
return feature['properties']
|
|
|
|
|
|
|
|
|
|
|
|
def get_address_from_coordinates(coordinates):
|
|
|
|
propertyPolygon = intersecting_property(coordinates)
|
|
|
|
if propertyPolygon is not None:
|
|
|
|
prop_geom = GEOSGeometry(json.dumps(propertyPolygon['geometry']))
|
|
|
|
return get_address(prop_geom)
|
2023-02-08 14:16:07 +13:00
|
|
|
|
|
|
|
|
|
|
|
def search_address(address):
|
|
|
|
# normalize accent characters etc.
|
|
|
|
address = normalize(
|
|
|
|
"NFKD",
|
|
|
|
address.strip().lower(),
|
|
|
|
).encode("ascii", "ignore").decode()
|
|
|
|
|
|
|
|
nums = search_num.findall(address)
|
|
|
|
strings = search_str.findall(address)
|
|
|
|
|
2023-02-21 16:43:46 +13:00
|
|
|
num_filter = Q()
|
|
|
|
str_filter = Q()
|
2023-02-08 14:16:07 +13:00
|
|
|
|
2023-02-21 16:43:46 +13:00
|
|
|
for n in nums:
|
|
|
|
num_filter |= Q(address_number=n)
|
2023-02-08 14:16:07 +13:00
|
|
|
|
2023-02-21 16:43:46 +13:00
|
|
|
for s in strings:
|
|
|
|
str_filter |= (
|
|
|
|
Q(full_road_name_ascii__istartswith=s) |
|
|
|
|
Q(town_city_ascii__istartswith=s) |
|
|
|
|
Q(suburb_locality_ascii__istartswith=s)
|
|
|
|
)
|
2023-02-08 14:16:07 +13:00
|
|
|
|
|
|
|
return [
|
|
|
|
{
|
2023-02-21 16:43:46 +13:00
|
|
|
'coordinates': (addr.wkb_geometry.x, addr.wkb_geometry.y),
|
|
|
|
'address': addr.full_address,
|
|
|
|
} for addr in Address.objects.filter(num_filter & str_filter).distinct()[:10]
|
2023-02-08 14:16:07 +13:00
|
|
|
]
|