import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
import folium
from shapely.geometry import Polygon, Point, MultiPolygon
from shapely import wkt
%matplotlib inline
Generate regular hex and square grids covering area of interest
27 May 2019
The basic approach is: - Define a flat CRS and a projected CRS. So x, y and lat, lng values respectively. - Write the code the create a Shapely Polygon that defines a regular hexagon (or square) with a particular radius in metres. - Define an area (in lat, lng values) over which we would like to create a tiling. - Intersect this area with the geographic area of interest
1.0. Setup
Import all the necessary libraries and then define the Coordinate Reference Systems (CRS) that we will be working with.
1.1. Coordinate Reference Systems
# The CRS that the ABS SA2 shapefile uses
= {'init': 'epsg:4283'}
crs_abs
# the projected CRS
= {'init': 'epsg:28355'}
crs_vic
# maybe try this one: epsg:3395
# south west most point of melb
= [143.338683, -39.185049]
p_lower
# north east most point of melb
= [146.742224, -36.559378]
p_upper
# put these in a Geoseries
= gpd.GeoSeries([Point(p_lower), Point(p_upper)], crs=crs_abs)
gs1
# convert each of these points to the 'flat coordinate system'
= gs1.to_crs(crs_vic) gs1b
2.0. Define the regular hexagonal grid
2.1. Define a regular hexagon in the flat geometry
Firstly we define a regular hexagon and then tile this over the given rectangle
# startpoint coords
= gs1b[0].x, gs1b[0].y
coords0
# the x and y extent of the grid
= gs1b[1].x - gs1b[0].x
Xdiff = gs1b[1].y - gs1b[0].y
Ydiff
# the cell width and height (in metres)
= 1000
celllength = celllength
r
# fundamental hexagon
= np.sqrt(3) / 2, 1 / 2
a, b = np.array([(0, r), (-r*a, r*b), (-r*a, -r*b), (0, -r), (r*a, -r*b), (r*a, r*b)])
poly_hex
# calculate the number of cells in the X and Y directions
= int(Xdiff / celllength / 2)
num_cells_x = int(Ydiff / celllength / 2) num_cells_y
2.2. Tile this hexagon over the rectangle covering the specified area.
Clean this up to void using the for loops.
# centres of a regular grid of hexagons of unit radius
= []
polys
# using a for loop
for m in range(0, num_cells_x):
for n in range(0, num_cells_y):
# generate the two sublattices
= np.array([coords0[0] + 2*m*a*r, coords0[1] + 3*2*n*b*r]).reshape(1,-1) # displacement vector
d +poly_hex))
polys.append(Polygon(d
= np.array([coords0[0] + (2*m+1)*a*r, coords0[1] + 3*(2*n+1)*b*r]).reshape(1,-1)
d +poly_hex))
polys.append(Polygon(d
= gpd.GeoSeries(polys, crs=crs_vic)
hexes
# convert to dataframe and change crs
= gpd.GeoDataFrame(geometry=hexes)
gdf_hex = gdf_hex.to_crs(crs_abs)
gdf_hex
# add a label for each hexagon for when we do a spatial join
'HexLabel'] = np.arange(gdf_hex.shape[0]) gdf_hex[
3.0 Define a regular square grid
# startpoint coords
= gs1b[0].x, gs1b[0].y
coords0
# the x and y extent of the grid
= gs1b[1].x - gs1b[0].x
Xdiff = gs1b[1].y - gs1b[0].y
Ydiff
# the cell width and height (in metres)
= 1000
celllength = celllength
r
# fundamental square
= 1, 1
a, b = np.array([(0, 0), (a, 0), (a, b), (0, b)])
poly_square
# calculate the number of cells in the X and Y directions
= int(Xdiff / celllength)
num_cells_x = int(Ydiff / celllength) num_cells_y
3.1. Tile this square over the rectangle covering the specified area.
# centres of a regular grid of hexagons of unit radius
= []
polys
# using a for loop
for m in range(0, num_cells_x):
for n in range(0, num_cells_y):
# generate the two sublattices
= np.array([coords0[0] + a*m*r, coords0[1] + b*n*r]).reshape(1,-1)
d +poly_square*r))
polys.append(Polygon(d
= gpd.GeoSeries(polys, crs=crs_vic)
squares
# convert to dataframe and change crs
= gpd.GeoDataFrame(geometry=squares)
gdf_sqr = gdf_sqr.to_crs(crs_abs)
gdf_sqr
# add a label for each hexagon for when we do a spatial join
'SquareLabel'] = np.arange(gdf_sqr.shape[0]) gdf_sqr[
4.0. Load up the Vic SA2 values and shapefiles
The SA2 files are here: https://www.abs.gov.au/AUSSTATS/[email protected]/DetailsPage/1270.0.55.003July%202016?OpenDocument
# load up the shape files for Vic and filter down to Victoria, removing null values
= gpd.read_file('data/shapefiles/SA2_2016_AUST.shp')
gdf_vic = gdf_vic[gdf_vic['geometry'].isnull() == False] # remove the null values gdf_vic
4.1. Area Of Interest
Define the Area Of Interest that we would like to restrict our grids to
# Greater Melbourne
= gdf_vic[gdf_vic['GCC_NAME16']=='Greater Melbourne']
gdf_gm
# The SA4's that make up Greater Melbourne and Geelong
= ['Melbourne - Inner', 'Melbourne - Inner East',
sa4_aoi 'Melbourne - Inner South', 'Melbourne - North East',
'Melbourne - North West', 'Melbourne - Outer East',
'Melbourne - South East', 'Melbourne - West',
'Mornington Peninsula']
= gdf_vic[gdf_vic['SA4_NAME16'].isin(sa4_aoi)] gdf_aoi
4.2. Intersect the hexgrid
# join hexagon grid with AOI
= gpd.sjoin(gdf_aoi, gdf_hex, op='intersects', how='inner') gdf_new
# just get the hexagons that cover melbourne
= gdf_hex[gdf_hex['HexLabel'].isin(gdf_new['HexLabel'].values)] hex_gm
# write to a shapefile
'hexgrid_aoi_1km.shp', index=False) hex_gm.to_file(
4.3. Intersect the squaregrid
# join hexagon grid with Greater Melbourne
= gpd.sjoin(gdf_aoi, gdf_sqr, op='intersects', how='inner') gdf_new
# just get the hexagons that cover melbourne
= gdf_sqr[gdf_sqr['SquareLabel'].isin(gdf_new['SquareLabel'].values)] sqr_gm
# write to a shapefile
'sqrgrid_aoi_1km.shp', index=False) sqr_gm.to_file(
5.0. Plot the regular hexgrids and square grids covering our AOI
5.1 Hexgrid
# initialize the map
= folium.Map(location=[-37.8, 145], zoom_start=8.5, tiles='cartodbpositron')
m
# add the hexagon grid to the map
folium.GeoJson(hex_gm.geometry).add_to(m)
# display all of this
m
5.2. Squaregrid
# initialize the map
= folium.Map(location=[-37.8, 145], zoom_start=8.5, tiles='cartodbpositron')
m
# add the hexagon grid to the map
folium.GeoJson(sqr_gm.geometry).add_to(m)
# display all of this
m
6.0. Validate the lengths using the haversine formula
from math import atan2
# from
# https://community.esri.com/groups/coordinate-reference-systems/blog/2017/10/05/haversine-formula
def haversine(coord1: object, coord2: object):
# Coordinates in decimal degrees (e.g. 2.89078, 12.79797)
= coord1
lon1, lat1 = coord2
lon2, lat2
= 6371000 # radius of Earth in meters
R = radians(lat1)
phi_1 = radians(lat2)
phi_2
= radians(lat2 - lat1)
delta_phi = radians(lon2 - lon1)
delta_lambda
= sin(delta_phi / 2.0) ** 2 + cos(phi_1) * cos(phi_2) * sin(delta_lambda / 2.0) ** 2
a
= 2 * atan2(sqrt(a), sqrt(1 - a))
c
= R * c # output distance in meters
meters = meters / 1000.0 # output distance in kilometers
km
= round(meters, 3)
meters = round(km, 3)
km
print(f"Distance: {meters} m")
print(f"Distance: {km} km")