Define a Voronoi cell max radius (python) - python

My question is similar to this one (R + ggplot) but for Python.
Question
How to generate a (2D) Voronoi diagram where cells have a maximum extend / growth limit ?
it would result in some cells with curved boundary (circle arc), and some space regions not filled by any cell.
An image processing interpretation of the desired output would be to perform some "AND" mask between the voronoi cells and circles centered on each cell with the required radius.
(For my particular problem, I am working with geolocated data, and for now I am willing to accept discrepancies from most voronoi libs that will use lat/lon as cartesian coordinates. That is another matter...)
This question is NOT about clipping or handling of "inifinite" voronoi cells (see scypi.spatial.Voronoi), so the following links are NOT related :
clipping a voronoi diagram python
How to limit Voronoi cells even when infinite with python?
Vaguely related topics:
Is there a way to vary the rate of Voronoi cell growth?
https://gis.stackexchange.com/questions/366471/weighted-voronoi-polygons-in-r
https://gis.stackexchange.com/questions/17282/create-weighted-thiessen-polygons/17284#17284
https://github.com/HichamZouarhi/Weighted-Voronoi-PyQGIS
Example from the R + ggplot answer:
sample code for tests:
from typing import Tuple, List, Union
import geovoronoi
import matplotlib.colors as clr
import matplotlib.pyplot as plt
import numpy as np
from geographiclib.geodesic import Geodesic
from shapely.geometry import Polygon, Point
from simplekml import LineStyle, Color, PolyStyle, Container
WGS84_Tool = Geodesic.WGS84
T_Color_List = List[Union[clr.Colormap, clr.LinearSegmentedColormap, clr.ListedColormap]]
def generate_n_colors(n, cmap_name='tab20') -> T_Color_List:
"""
https://github.com/WZBSocialScienceCenter/geovoronoi/blob/master/geovoronoi/plotting.py
Get a list of `n` numbers from matplotlib color map `cmap_name`. If `n` is larger than the number of colors in the
color map, the colors will be recycled, i.e. they are not unique in this case.
:param n: number of colors to generate
:param cmap_name: matplotlib color map name
:return: list of `n` colors
"""
pt_region_colormap = plt.get_cmap(cmap_name)
max_i = len(pt_region_colormap.colors)
return [pt_region_colormap(i % max_i) for i in range(n)]
def _plot_cell_and_seed(kml: Container,
name: str,
region_polygon: Polygon,
seed_coords: Tuple[float, float],
_kml_color: str):
_p = kml.newpolygon(name=f"{name} zone",
outerboundaryis=[(lon, lat)
for lat, lon
in region_polygon.exterior.coords], )
_p.style.linestyle = LineStyle(color=Color.darkgrey, width=1.)
_p.style.polystyle = PolyStyle(color=_kml_color)
p = kml.newpoint(coords=[seed_coords],
name=name)
p.style.iconstyle.icon.href = "http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png"
p.style.iconstyle.scale = 0.5
p.style.labelstyle.scale = 0.5
def plot_regions(region_polys,
region_pts,
seeds_coords_list: np.ndarray,
seeds_names: List[str],
kml: Container,
colors: T_Color_List,
):
assert (len(seeds_names) == len(seeds_coords_list))
index = 0
for region_id, region_polygon in region_polys.items():
_cell_point_indexes = region_pts[region_id]
_cell_seed_coords = seeds_coords_list[_cell_point_indexes][0]
name = seeds_names[_cell_point_indexes[0]]
_kml_airport_coords = (_cell_seed_coords[-1], _cell_seed_coords[0])
_mpl_color = colors[index]
_mpl_hexa_color = clr.to_hex(_mpl_color, keep_alpha=True)
_hexa_color_no_sharp = _mpl_hexa_color.split("#")[-1]
_kml_color = Color.hexa(_hexa_color_no_sharp)
_kml_color = Color.changealphaint(alpha=7 * 255 // 10, gehex=_kml_color)
_plot_cell_and_seed(kml=kml,
name=name,
region_polygon=region_polygon,
seed_coords=_kml_airport_coords,
_kml_color=_kml_color)
index += 1
# bounding box for geovoronoi
geo_boundaries = {"min": {"lat": +30, "lon": -12},
"max": {"lat": +75, "lon": +35}, }
# a list of [[lat,lon],[lat,lon],[lat,lon],[lat,lon],]
n = 150
seeds_coords_list = np.dstack(
[np.random.uniform(low=geo_boundaries["min"]["lat"], high=geo_boundaries["max"]["lat"], size=n),
np.random.uniform(low=geo_boundaries["min"]["lon"], high=geo_boundaries["max"]["lon"], size=n), ]).reshape((n, 2))
seeds_names = [f"{lat:+_.2f};{lon:+_.2f}" for lat, lon in seeds_coords_list]
boundary_points = geovoronoi.coords_to_points([[geo_boundaries["min"]["lat"], geo_boundaries["min"]["lon"]],
[geo_boundaries["min"]["lat"], geo_boundaries["max"]["lon"]],
[geo_boundaries["max"]["lat"], geo_boundaries["max"]["lon"]],
[geo_boundaries["max"]["lat"], geo_boundaries["min"]["lon"]],
# last necessary ?
[geo_boundaries["min"]["lat"], geo_boundaries["min"]["lon"]]])
boundary_polygon = Polygon(boundary_points)
# beware that geodesics and geovoronoi may not be accurate since it uses planar cartesian formulas...
region_polys, region_pts = geovoronoi.voronoi_regions_from_coords(seeds_coords_list, boundary_polygon)
# DO SOMETHING HERE
#...
#...
#...
kdoc = Kml()
p_kml = Path.cwd() / "voronoi_so.kml"
colors: T_Color_List = generate_n_colors(len(region_polys))
plot_regions(region_polys=region_polys,
region_pts=region_pts,
seeds_coords_list=seeds_coords_list,
seeds_names=seeds_names,
kml=kdoc.newfolder(name="raw regions"),
colors=colors)
print("save KML")
kdoc.save(p_kml.as_posix())
print(p_kml.as_uri())

After some thinking, here is an approach using Shapely and using the intersection between computed circles (360 vertices) and each voronoi cell.
Performance is not convincing since we create 360 points and a polygon + 1 intersection computation for each cell... (at least it is a bounded ...).
However, the underlying voronoi computation data such as cells adjacency is lost, and we can't know if some cells are isolated after this transformation. Maybe some ideas here: Determining and storing Voronoi Cell Adjacency
I do believe better solutions may exist.
So here is a solution, with random geographical data, and corresponding kml rendering.
EDIT: using weighted voronoi diagrams, a possible weight function could be f(d) = d <= radius ? I could not explore this for now...
Raw regions:
Modified raw regions:
from pathlib import Path
from typing import Tuple, List, Union
import geovoronoi
import matplotlib.colors as clr
import matplotlib.pyplot as plt
import numpy as np
from geographiclib.geodesic import Geodesic
from shapely.geometry import Polygon, Point
from simplekml import LineStyle, Kml, Color, PolyStyle, Container
WGS84_Tool = Geodesic.WGS84
T_Color_List = List[Union[clr.Colormap, clr.LinearSegmentedColormap, clr.ListedColormap]]
def generate_n_colors(n, cmap_name='tab20') -> T_Color_List:
"""
https://github.com/WZBSocialScienceCenter/geovoronoi/blob/master/geovoronoi/plotting.py
Get a list of `n` numbers from matplotlib color map `cmap_name`. If `n` is larger than the number of colors in the
color map, the colors will be recycled, i.e. they are not unique in this case.
:param n: number of colors to generate
:param cmap_name: matplotlib color map name
:return: list of `n` colors
"""
pt_region_colormap = plt.get_cmap(cmap_name)
max_i = len(pt_region_colormap.colors)
return [pt_region_colormap(i % max_i) for i in range(n)]
def _create_bounded_regions(region_polys,
region_pts,
distance_criteria_m: float,
cell_seed_coords_list: np.ndarray,
nb_vertices:int=36):
new_polygons = {}
for region_id, region_polygon in region_polys.items():
_cell_point_indexes = region_pts[region_id]
_cell_seed_coords = cell_seed_coords_list[_cell_point_indexes][0]
_arpt_lat = _cell_seed_coords[0]
_arpt_lon = _cell_seed_coords[-1]
cycle_vertices = []
for a in np.linspace(0,359,nb_vertices):
p = WGS84_Tool.Direct(lat1=_arpt_lat, lon1=_arpt_lon, azi1=a, s12=distance_criteria_m)
_point = Point(p["lat2"], p["lon2"])
cycle_vertices.append(_point)
circle = Polygon(cycle_vertices)
new_polygons[region_id] = region_polygon.intersection(circle)
return new_polygons
def _plot_cell_and_seed(kml: Container,
name: str,
region_polygon: Polygon,
seed_coords: Tuple[float, float],
_kml_color: str):
_p = kml.newpolygon(name=f"{name} zone",
outerboundaryis=[(lon, lat)
for lat, lon
in region_polygon.exterior.coords], )
_p.style.linestyle = LineStyle(color=Color.darkgrey, width=1.)
_p.style.polystyle = PolyStyle(color=_kml_color)
p = kml.newpoint(coords=[seed_coords],
name=name)
p.style.iconstyle.icon.href = "http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png"
p.style.iconstyle.scale = 0.5
p.style.labelstyle.scale = 0.5
def plot_regions(region_polys,
region_pts,
seeds_coords_list: np.ndarray,
seeds_names: List[str],
kml: Container,
colors: T_Color_List,
):
assert (len(seeds_names) == len(seeds_coords_list))
index = 0
for region_id, region_polygon in region_polys.items():
_cell_point_indexes = region_pts[region_id]
_cell_seed_coords = seeds_coords_list[_cell_point_indexes][0]
name = seeds_names[_cell_point_indexes[0]]
_kml_airport_coords = (_cell_seed_coords[-1], _cell_seed_coords[0])
_mpl_color = colors[index]
_mpl_hexa_color = clr.to_hex(_mpl_color, keep_alpha=True)
_hexa_color_no_sharp = _mpl_hexa_color.split("#")[-1]
_kml_color = Color.hexa(_hexa_color_no_sharp)
_kml_color = Color.changealphaint(alpha=7 * 255 // 10, gehex=_kml_color)
_plot_cell_and_seed(kml=kml,
name=name,
region_polygon=region_polygon,
seed_coords=_kml_airport_coords,
_kml_color=_kml_color)
index += 1
# bounding box for geovoronoi
geo_boundaries = {"min": {"lat": +30, "lon": -12},
"max": {"lat": +75, "lon": +35}, }
# a list of [[lat,lon],[lat,lon],[lat,lon],[lat,lon],]
n = 150
seeds_coords_list = np.dstack(
[np.random.uniform(low=geo_boundaries["min"]["lat"], high=geo_boundaries["max"]["lat"], size=n),
np.random.uniform(low=geo_boundaries["min"]["lon"], high=geo_boundaries["max"]["lon"], size=n), ]).reshape((n, 2))
seeds_names = [f"{lat:+_.2f};{lon:+_.2f}" for lat, lon in seeds_coords_list]
boundary_points = geovoronoi.coords_to_points([[geo_boundaries["min"]["lat"], geo_boundaries["min"]["lon"]],
[geo_boundaries["min"]["lat"], geo_boundaries["max"]["lon"]],
[geo_boundaries["max"]["lat"], geo_boundaries["max"]["lon"]],
[geo_boundaries["max"]["lat"], geo_boundaries["min"]["lon"]],
# last necessary ?
[geo_boundaries["min"]["lat"], geo_boundaries["min"]["lon"]]])
boundary_polygon = Polygon(boundary_points)
# beware that geodesics and geovoronoi may not be accurate since it uses planar cartesian formulas...
region_polys, region_pts = geovoronoi.voronoi_regions_from_coords(seeds_coords_list, boundary_polygon)
new_region_polys = _create_bounded_regions(region_polys=region_polys,
region_pts=region_pts,
distance_criteria_m=300_000.,
cell_seed_coords_list=seeds_coords_list, )
kdoc = Kml()
p_kml = Path.cwd() / "voronoi_so.kml"
colors: T_Color_List = generate_n_colors(len(region_polys))
plot_regions(region_polys=region_polys,
region_pts=region_pts,
seeds_coords_list=seeds_coords_list,
seeds_names=seeds_names,
kml=kdoc.newfolder(name="raw regions"),
colors=colors)
plot_regions(region_polys=new_region_polys,
region_pts=region_pts,
seeds_coords_list=seeds_coords_list,
seeds_names=seeds_names,
kml=kdoc.newfolder(name="new_regions (range)"),
colors=colors)
print("save KML")
kdoc.save(p_kml.as_posix())
print(p_kml.as_uri())

Related

Folium plot GeoJson fill color in polygon based on custom values

I have polygons with lat/long values associated with identifiers in a GeoDataFrame as shown below. Consider an example with two identifiers A and B, polygon A has three points and B has four points, their lat/long values are as shown below. Corresponding to each point (lat/long), I also have an associated numeric value as shown in the last column.
id geometry values
A POLYGON((lat_A_1 long_A_1, lat_A_2 long_A_2, lat_A_3 long_A_3)) 10,12,13
B POLYGON((lat_B_1 long_B_1, lat_B_2 long_B_2, lat_B_3 long_B_3, lat_B_4 long_B_4)) 4,8,16,20
I iterate over the GeoDataFrame and plot these polygons on the map using this code
geo_j = folium.GeoJson(data=geo_j,
style_function={
'fillColor': 'blue'
})
Is there a way that I can fill the polygon with a custom colormap based on the column values in the GeoDataFrame, such as red for 0-5, blue for 6-10 and green for 11-20. How can this be done?
started by getting some polygons and defining a value per point (generate MWE sample data set)
this means you have as many values associated with polygon as there are points in the polygon. You request a solution using folium that fills the polygon with a custom color map value. This means you need to have a function that will assimilates all these values into a single value for the polygon (a color). I have used mode, most common value. This could be mean, median or any other function.
solution then become simple, it's folium.GeoJson() using and appropriately structured style_function
extended answer. You can split polygon into smaller polygons and associate color of sub-polygon with a point. folium production is unchanged (have include iso_a3) just to make it simpler to view
shapley provides two ways to split a polygon https://shapely.readthedocs.io/en/stable/manual.html#shapely.ops.triangulate. Have found that voronoi is more effective
generate MWE data
# some polygons
gdf = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")).loc[lambda d: d["iso_a3"].isin(["BEL", "LUX", "NLD", "DEU", "AUT"]), ["geometry"]]
# comma separated values column... between 0 and 20...
gdf["values"] = gdf.geometry.apply(lambda p: ",".join([str(int(sum(xy)) % 20) for xy in p.exterior.coords]))
# id column
gdf["id"] = list("ABCDEFGHIJ")[0 : len(gdf)]
gdf = gdf.set_index("id", drop=False)
data
geometry values id
id
A POLYGON ((16.97967 48.12350, 16.9037... 5,4,4,4,3,2,1,1,0,19,19,18,17,17,16,... A
B POLYGON ((14.11969 53.75703, 14.3533... 7,7,7,7,6,6,6,5,5,4,4,3,2,2,2,2,2,1,... B
C POLYGON ((6.04307 50.12805, 6.24275 ... 16,16,15,15,15,15,16 C
D POLYGON ((6.15666 50.80372, 6.04307 ... 16,16,15,15,14,14,13,13,13,13,14,14,... D
E POLYGON ((6.90514 53.48216, 7.09205 ... 0,0,19,18,17,16,16,16,15,14,14,15,17... E
solution
import statistics as st
import branca.colormap
import geopandas as gpd
import folium
m = folium.Map(
location=[
sum(gdf.geometry.total_bounds[[1, 3]]) / 2,
sum(gdf.geometry.total_bounds[[0, 2]]) / 2,
],
zoom_start=5,
control_scale=True,
)
# style the polygons based on "values" property
def style_fn(feature):
cm = branca.colormap.LinearColormap(["mistyrose", "tomato", "red"], vmin=0, vmax=20)
most_common = st.mode([int(v) for v in feature["properties"]["values"].split(",")])
ss = {
"fillColor": cm(most_common),
"fillOpacity": 0.8,
"weight": 0.8,
"color": cm(most_common),
}
return ss
folium.GeoJson(
gdf.__geo_interface__,
style_function=style_fn,
tooltip=folium.features.GeoJsonTooltip(["id", "values"]),
).add_to(m)
m
split polygons into parts
import statistics as st
import branca.colormap
import geopandas as gpd
import folium
import shapely.geometry
import shapely.ops
import pandas as pd
# some polygons
# fmt: off
gdf = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres")).loc[lambda d: d["iso_a3"].isin(["BEL", "LUX", "NLD", "DEU", "AUT","POL"]), ["geometry", "iso_a3"]]
# comma separated values column... between 0 and 20...
gdf["values"] = gdf.geometry.apply(lambda p: ",".join([str(int(sum(xy)) % 20) for xy in p.exterior.coords]))
# id column
gdf["id"] = list("ABCDEFGHIJ")[0 : len(gdf)]
gdf = gdf.set_index("id", drop=False)
# fmt: on
def sub_polygons(r, method="voronoi"):
g = r["geometry"]
# split into sub-polygons
if method == "voronoi":
geoms = shapely.ops.voronoi_diagram(g).geoms
elif method == "triangulate":
geoms = [
p
for p in shapely.ops.triangulate(g)
if isinstance(p.intersection(g), shapely.geometry.Polygon)
]
else:
raise "invalid polygon ops method"
# clip sub-geometries
geoms = [p.intersection(g) for p in geoms]
vs = r["values"].split(",")
vr = []
# order or sub-polygons and points are differenct. use value from point
# in sub-polygon
for vg in geoms:
for i, xy in enumerate(g.exterior.coords):
if not shapely.geometry.Point(xy).intersection(vg).is_empty:
break
vr.append(vs[i])
return [{**r.to_dict(), **{"geometry": g, "values": v}} for g, v in zip(geoms, vr)]
gdf2 = gpd.GeoDataFrame(
gdf.apply(sub_polygons, axis=1, method="voronoi").explode().apply(pd.Series)
)
m = folium.Map(
location=[
sum(gdf.geometry.total_bounds[[1, 3]]) / 2,
sum(gdf.geometry.total_bounds[[0, 2]]) / 2,
],
zoom_start=5,
control_scale=True,
)
# style the polygons based on "values" property
def style_fn(feature):
cm = branca.colormap.LinearColormap(["mistyrose", "tomato", "red"], vmin=0, vmax=20)
most_common = st.mode([int(v) for v in feature["properties"]["values"].split(",")])
ss = {
"fillColor": cm(most_common),
"fillOpacity": 0.8,
"weight": 0.8,
"color": cm(most_common),
}
return ss
folium.GeoJson(
gdf2.__geo_interface__,
style_function=style_fn,
tooltip=folium.features.GeoJsonTooltip(["id", "values", "iso_a3"]),
).add_to(m)
m
with FeatureGroup
m = folium.Map(
location=[
sum(gdf.geometry.total_bounds[[1, 3]]) / 2,
sum(gdf.geometry.total_bounds[[0, 2]]) / 2,
],
zoom_start=5,
control_scale=True,
)
for g, d in gdf2.groupby(level=0):
fg = folium.map.FeatureGroup(name=g)
folium.GeoJson(
d.__geo_interface__,
style_function=style_fn,
tooltip=folium.features.GeoJsonTooltip(["id", "values", "iso_a3"]),
).add_to(fg)
fg.add_to(m)
folium.LayerControl().add_to(m)
m

How to find nearest node along nearest edge?

I am using this script from here: link
I want to add new functionality to it. I want it to define starting node on the graph not only by finding the closest node (because that produces odd results like finding the closest node on an other road) but finding the closest and and the closest node along that edge.
My code is available below. I created the findnearestnodeonnearestedge function which should do the work but it doesn't work.
It finds the same node for the starting and destination point, even though they are far from each other...
I am using the newest versions of all packages so you can try the code easily.
Thank you for help
import osmnx as ox
import networkx as nx
import plotly.graph_objects as go
import numpy as np
def findnearestnodeonnearestedge(Gr, pointin):
u, v, key = ox.distance.nearest_edges(G, pointin[0], pointin[1])
n1 = Gr.nodes[u]
n2 = Gr.nodes[v]
d1 = ox.distance.euclidean_dist_vec(pointin[0], pointin[1], n1['x'], n1['y'])
d2 = ox.distance.euclidean_dist_vec(pointin[0], pointin[1], n2['x'], n2['y'])
if d1 < d2:
nodeid = u
else:
nodeid = v
return nodeid
state = ox.geocode_to_gdf('Georgia, US')
ax = ox.project_gdf(state).plot(fc='gray', ec='none')
_ = ax.axis('off')
# Defining the map boundaries
north, east, south, west = 33.798, -84.378, 33.763, -84.422
# Downloading the map as a graph object
G = ox.graph_from_bbox(north, south, east, west, network_type = 'drive')
# Plotting the map graph
ox.plot_graph(G)
# Displaying the 3rd node
list(G.nodes(data=True))[2]
# Displaying the 1st edge
list(G.edges(data=True))[1]
# Displaying the shape of edge using the geometry
list(G.edges(data=True))[1][2]['geometry']
# define origin and desination locations
origin_point = (33.787201, -84.405076)
destination_point = (33.764135, -84.394980)
# get the nearest nodes to the locations
origin_node = findnearestnodeonnearestedge(G, origin_point)
destination_node = findnearestnodeonnearestedge(G, destination_point)
# printing the closest node id to origin and destination points
origin_node, destination_node
# Finding the optimal path
route = nx.shortest_path(G, origin_node, destination_node, weight = 'length')
route
# getting coordinates of the nodes
# we will store the longitudes and latitudes in following list
long = []
lat = []
for i in route:
point = G.nodes[i]
long.append(point['x'])
lat.append(point['y'])
def plot_path(lat, long, origin_point, destination_point):
"""
Given a list of latitudes and longitudes, origin
and destination point, plots a path on a map
Parameters
----------
lat, long: list of latitudes and longitudes
origin_point, destination_point: co-ordinates of origin
and destination
Returns
-------
Nothing. Only shows the map.
"""
# adding the lines joining the nodes
fig = go.Figure(go.Scattermapbox(
name="Path",
mode="lines",
lon=long,
lat=lat,
marker={'size': 10},
line=dict(width=4.5, color='blue')))
# adding source marker
fig.add_trace(go.Scattermapbox(
name="Source",
mode="markers",
lon=[origin_point[1]],
lat=[origin_point[0]],
marker={'size': 12, 'color': "red"}))
# adding destination marker
fig.add_trace(go.Scattermapbox(
name="Destination",
mode="markers",
lon=[destination_point[1]],
lat=[destination_point[0]],
marker={'size': 12, 'color': 'green'}))
# getting center for plots:
lat_center = np.mean(lat)
long_center = np.mean(long)
# defining the layout using mapbox_style
fig.update_layout(mapbox_style="stamen-terrain", mapbox_center_lat=30, mapbox_center_lon=-80)
fig.update_layout(margin={"r": 0, "t": 0, "l": 0, "b": 0},
mapbox={
'center': {'lat': lat_center, 'lon': long_center},
'zoom': 13})
fig.show()
plot_path(lat, long, origin_point, destination_point)
# Getting the start and end node of this part
start_node=route[-7]
end_node=route[-6]
# Getting the edge connecting these nodes and storing it as a list in z to maintain the data structure of G.edges
z = []
for i in list(G.edges(data=True)):
if (i[0]==start_node) & (i[1]==end_node):
z.append(i)
z[0][2]['geometry']
def node_list_to_path(G, node_list):
"""
Given a list of nodes, return a list of lines that together follow the path
defined by the list of nodes.
Parameters
----------
G : networkx multidigraph
route : list
the route as a list of nodes
Returns
-------
lines : list of lines given as pairs ( (x_start, y_start), (x_stop, y_stop) )
"""
edge_nodes = list(zip(node_list[:-1], node_list[1:]))
lines = []
for u, v in edge_nodes:
# if there are parallel edges, select the shortest in length
data = min(G.get_edge_data(u, v).values(), key=lambda x: x['length'])
# if it has a geometry attribute (ie, a list of line segments)
if 'geometry' in data:
# add them to the list of lines to plot
xs, ys = data['geometry'].xy
lines.append(list(zip(xs, ys)))
else:
# if it doesn't have a geometry attribute, the edge is a straight
# line from node to node
x1 = G.nodes[u]['x']
y1 = G.nodes[u]['y']
x2 = G.nodes[v]['x']
y2 = G.nodes[v]['y']
line = [(x1, y1), (x2, y2)]
lines.append(line)
return lines
# getting the list of coordinates from the path (which is a list of nodes)
lines = node_list_to_path(G, route)
long2 = []
lat2 = []
for i in range(len(lines)):
z = list(lines[i])
l1 = list(list(zip(*z))[0])
l2 = list(list(zip(*z))[1])
for j in range(len(l1)):
long2.append(l1[j])
lat2.append(l2[j])
print("Length of lat: ", len(lat))
print("Length of lat2: ", len(lat2))
plot_path(lat2, long2, origin_point, destination_point)
Problem was that in the graph coordinates are stored in reverse order. So all pointin[0] and pointin[1] in the function should be reversed and then it will work

Edit polygon coords using Python, Shapely and Fiona

I need to edit the geometry of intersecting polygons and I don't know how I can save modified geometry to a shapefile. Is it even possible?
from shapely.geometry import Polygon, shape
import matplotlib.pyplot as plt
import fiona
c = fiona.open('polygon23.shp', 'r')
d = fiona.open('polygon23.shp', 'r')
for poly in c.values():
for poly2 in d.values():
p_poly = shape(poly['geometry'])
p_poly2 = shape(poly2['geometry'])
intersect_polygons = p_poly.intersection(p_poly2)
if type(intersect_polygons) == Polygon:
intersect_polygons = p_poly.intersection(p_poly2).exterior.coords
if p_poly.exterior.xy != p_poly2.exterior.xy:
y_difference = abs(intersect_polygons[0][1]) - abs(intersect_polygons[2][1])
coords_polygonB = p_poly2.exterior.coords[:]
coords_polygonB[0] = (coords_polygonB[0][0], coords_polygonB[0][1] + (y_difference))
coords_polygonB[1] = (coords_polygonB[1][0], coords_polygonB[1][1] + (y_difference))
coords_polygonB[2] = (coords_polygonB[2][0], coords_polygonB[2][1] + (y_difference))
coords_polygonB[3] = (coords_polygonB[3][0], coords_polygonB[3][1] + (y_difference))
coords_polygonB[4] = (coords_polygonB[4][0], coords_polygonB[4][1] + (y_difference))
p_poly2 = Polygon(coords_polygonB)
x,y = p_poly.exterior.xy
plt.plot(x,y)
x,y = p_poly2.exterior.xy
plt.plot(x,y)
plt.show()
The removal of intersections between many polygons is most likely a complex problem. Moreover, I used your method as the solver in my solution.
Answer
The answer to your question, is yes. You can rectify the intersections between the polygons in your shp file; however, you need to create new Polygon objects, you can't just change the exterior coordinates of an existing Polygon.
Store metadata and disc from original shp file
The solution below writes/creates the resulting polygon set to a new shp file. This requires us to store the metadata from the original shp file, and pass it to the new one. We also need to store the properties of each polygon, I store these in a separate list, set_of_properties.
No need for two for loops
You don't need to for loops, just use combinations from the itertools standard library to loop through all possible polygon combinations. I use index combinations to replace polygons that are intersecting with new ones.
Outer do...while-loop
In very cringe caes, a rectification using your method may actually introduce new intersections. We can catch these and rectify them by looping through your solver until there are no intersections left. This requires a do... while loop, but there is no do...while loop in Python. Moreover, it can be implemented with while-loops (see Solution for implementation).
Solution
from itertools import combinations
from shapely.geometry import Polygon, Point, shape, mapping
import matplotlib.pyplot as plt
import fiona
SHOW_NEW_POLYGONS = False
polygons, set_of_properties = [], []
with fiona.open("polygon23.shp", "r") as source:
for line in source:
polygons.append(shape(line["geometry"]))
set_of_properties.append(line["properties"])
meta = source.meta
poly_index_combinations = combinations(tuple(range(len(polygons))), 2)
while True:
intersection_record = []
for i_poly_a, i_poly_b in poly_index_combinations:
poly_a, poly_b = polygons[i_poly_a], polygons[i_poly_b]
if poly_a.exterior.xy == poly_b.exterior.xy:
# print(f"The polygons have identical exterior coordinates:\n{poly_a} and {poly_b}\n")
continue
intersecting = poly_a.intersection(poly_b)
if type(intersecting) != Polygon:
continue
intersecting_polygons = intersecting.exterior.coords
if not intersecting_polygons:
# print(f"No intersections between\n{poly_a} and {poly_b}\n")
continue
print("Rectifying intersection")
y_diff = abs(intersecting_polygons[0][1]) - abs(intersecting_polygons[2][1])
new_poly_b = Polygon((
Point(float(poly_b.exterior.coords[0][0]), float(poly_b.exterior.coords[0][1] + y_diff)),
Point(float(poly_b.exterior.coords[1][0]), float(poly_b.exterior.coords[1][1] + y_diff)),
Point(float(poly_b.exterior.coords[2][0]), float(poly_b.exterior.coords[2][1] + y_diff)),
Point(float(poly_b.exterior.coords[3][0]), float(poly_b.exterior.coords[3][1] + y_diff)),
Point(float(poly_b.exterior.coords[4][0]), float(poly_b.exterior.coords[4][1] + y_diff))
))
if SHOW_NEW_POLYGONS:
x, y = poly_a.exterior.xy
plt.plot(x, y)
x, y = new_poly_b.exterior.xy
plt.plot(x, y)
plt.show()
polygons[i_poly_b] = new_poly_b
intersection_record.append(True)
if not intersection_record:
break
with fiona.open("new.shp", "w", **meta) as sink:
for poly, properties in zip(polygons, set_of_properties):
sink.write({
"geometry": mapping(poly),
"properties": properties
})

Python : shapely, cascaded intersections within one polygon

I'd like to split a polygon into a list of polygons corresponding to all intersections with other polygons (and intersections between themselves).
from shapely.geometry import Point
circleA = Point((0, 0)).buffer(1)
circleB = Point((1, 0)).buffer(1)
circleC = Point((1, 1)).buffer(1)
def cascaded_intersections(poly1, lst_poly):
# ???
return result
result = cascaded_intersections(circleA, (circleB, circleC))
The result should be a list of 4 Polygons, corresponding to the 4 complementary parts of A (above: [AC!B, ABC, AB!C, rest of A]).
The problem is the same than spitting a polygon into its smallest parts from a list of covering LineStrings.
How to write cascaded_intersections ?
A colleague of mine, Pascal L., found a solution :
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from shapely.geometry import MultiPolygon, Polygon, Point, GeometryCollection
from shapely.ops import cascaded_union
EMPTY = GeometryCollection()
def partition(poly_a, poly_b):
"""
Splits polygons A and B into their differences and intersection.
"""
if not poly_a.intersects(poly_b):
return poly_a, poly_b, EMPTY
only_a = poly_a.difference(poly_b)
only_b = poly_b.difference(poly_a)
inter = poly_a.intersection(poly_b)
return only_a, only_b, inter
def eliminate_small_areas(poly, small_area):
"""
Eliminates tiny parts of a MultiPolygon (or Polygon)
"""
if poly.area < small_area:
return EMPTY
if isinstance(poly, Polygon):
return poly
assert isinstance(poly, MultiPolygon)
l = [p for p in poly if p.area > small_area]
if len(l) == 0:
return EMPTY
if len(l) == 1:
return l[0]
return MultiPolygon(l)
def cascaded_intersections(poly1, lst_poly):
"""
Splits Polygon poly1 into intersections of/with list of other polygons.
"""
result = [(lst_poly[0], (0,))]
for i, poly in enumerate(lst_poly[1:], start=1):
current = []
while result:
result_geometry, result_indexes = result.pop(0)
only_result, only_poly, inter = partition(result_geometry, poly)
for geometry, indexes in ((only_result, result_indexes), (inter, result_indexes + (i,))):
if not geometry.is_empty:
current.append((geometry, indexes))
current_union = cascaded_union([elt[0] for elt in current])
only_poly = poly.difference(current_union)
if not only_poly.is_empty:
current.append((only_poly, (i,)))
result = current
for r in range(len(result)-1, -1, -1):
geometry, indexes = result[r]
if poly1.intersects(geometry):
inter = poly1.intersection(geometry)
result[r] = (inter, indexes)
else:
del result[r]
only_poly1 = poly1.difference(cascaded_union([elt[0] for elt in result]))
only_poly1 = eliminate_small_areas(only_poly1, 1e-16*poly1.area)
if not only_poly1.is_empty:
result.append((only_poly1, None))
return [r[0] for r in result]
a=Point(0,0).buffer(1)
b1=Point(0,1).buffer(1)
b2=Point(1,0).buffer(1)
b3=Point(1,1).buffer(1)
result = cascaded_intersections(a, (b1,b2,b3))
Hi hi again, here's a better solution than my own, using part of gene's answer # stackexchange.com it uses shapely.ops functions cascaded_union, unary_union and polygonize.
import matplotlib.pyplot as plt
import numpy as np
import shapely.geometry as sg
from shapely.ops import cascaded_union, unary_union, polygonize
import shapely.affinity
import descartes
from itertools import combinations
circleA = sg.Point((0, 0)).buffer(1)
circleB = sg.Point((1, 0)).buffer(1)
circleC = sg.Point((1, 1)).buffer(1)
circles = [circleA,circleB,circleC]
listpoly = [a.intersection(b) for a, b in combinations(circles, 2)] #list of intersections
rings = [sg.LineString(list(pol.exterior.coords)) for pol in listpoly] #list of rings
union = unary_union(rings)
result = [geom for geom in polygonize(union)] #list all intersection geometries
multi = cascaded_union(result) #Create a single geometry out of all intersections
fin = [c.difference(multi) for c in circles] #Cut multi from circles and leave only outside geometries.
result = result + fin #add the outside geometries to the intersections geometries
#Plot settings:
plt.figure(figsize=(5,5))
ax = plt.gca()
name = 1
for e in result:
ax.add_patch(descartes.PolygonPatch(e,
fc=np.random.rand(3),
ec=None,
alpha=0.5))
ax.text(e.centroid.x,e.centroid.y,
'%s'%name,fontsize=9,
bbox=dict(facecolor='orange', alpha=0.5),
color='blue',
horizontalalignment='center')
name += 1
plt.xlim(-1.5,2.5)
plt.ylim(-1.5,2.5)
plt.show()
Como va. Hi Eric, I tried using the split function from shapely.ops. Here is the result. This is not the most time efficient or elegant solution but it works:
import matplotlib.pyplot as plt
import numpy as np #use np.random to give random RGB color to each polygon
import shapely.geometry as sg
from shapely.ops import split
import descartes
from itertools import combinations
def cascade_split(to_split,splitters): #Helper function for split recursion
'''
Return a list of all intersections between multiple polygons.
to_split: list, polygons or sub-polygons to split
splitters: list, polygons used as splitters
Returns a list of all the polygons formed by the multiple intersections.
'''
if len(splitters) == 0: # Each splitting geometry will be removed
return to_split # at the end of the function, reaching len == 0 at some point,
# only the it will return all the final splits.
new_to_split = [] # make a list that will run again though the function
for ts in to_split:
s = split(ts,splitters[0].boundary) # split geometry using the boundaries of another
for i in list(s):
new_to_split.append(i) #save the splits
splitters.remove(splitters[0]) #remove the splitting geometry to
#allow the split with the next polygon in line.
return cascade_split(new_to_split,splitters) #Use recursion to exhaust all splitting possibilities
#Create polygons, in this case circles.
circleA = sg.Point((0, 0)).buffer(1)
circleB = sg.Point((1, 0)).buffer(1)
circleC = sg.Point((1, 1)).buffer(1)
#Put all circles in list
circles = [circleA,circleB,circleC]
#The combinations tool takes the last polygon
#from list to split with the remaning polygons in list,
#creating a backwards copy of the circles list will help keep track of shapes.
back_circles = circles[::-1] #backwards copy of circles list
index_count = 0 #Keep track of which circle will get splitted
polys = [] #Final list of splitted polygons
for i in combinations(circles,len(circles)-1):
c_split = cascade_split([back_circles[index_count]],list(i)) #Use helper function here
for p in c_split:
#There will be duplicate polygon splits, the following condition will filter those:
if not any(poly.equals(p) for poly in polys):
polys.append(p)
index_count += 1
#plotting settings
plt.figure(figsize=(5,5))
ax = plt.gca()
for e in range(len(polys)):
ax.add_patch(descartes.PolygonPatch(polys[e],
fc=np.random.rand(3), #give random color to each split
ec=None,
alpha=0.5))
ax.text(polys[e].centroid.x,polys[e].centroid.y,
'%s' %(e+1),fontsize=9,
bbox=dict(facecolor='orange', alpha=0.5),
color='blue',
horizontalalignment='center')
plt.xlim(-1.5,2.5)
plt.ylim(-1.5,2.5)
plt.show()
polys #Output the polys list to see all the splits

Plot boundaries of specific region in basemap

I have defined a region of interest where I have tracked e.g. evaporation in time. Now i want to depict this region on a basemap plot by plotting only its boundaries. The region is defined as an (almost) global lat/lon array filled only with 1 at the Region's coordinates (like a land/sea mask, but for my specific region).
If people want to plot boundaries of a certain geometry they often refer to shapefiles (which i am unfamiliar with), but it seems an easy way to create a polygon and plot this polygon on a basemap. However, i cannot find info on creating a shapefile from an array similar to my 'Region array'.
What is your suggestion?
Thanks for the responses, i indeed solved it with making a polygon with the coordinates of the edge-gridcells of my region.
{
import numpy as np
from netCDF4 import Dataset
def getRegion(latnrs,lonnrs, latitude, longitude, lsm):
lsm_globe = lsm
for lat in range(0,len(latitude)):
for lon in range(0,len(longitude)):
if longitude[lon] < 1.5:
lsm_globe[lat,lon] = 0.
if longitude[lon] > 15:
lsm_globe[lat,lon] = 0.
if latitude[lat] < 48:
lsm_globe[lat,lon] = 0.
if latitude[lat] > 54:
lsm_globe[lat,lon] = 0.
Region = lsm_globe
import matplotlib.path as mpath
coord_region = np.argwhere(Region>0)
lats = np.zeros(len(coord_region))
lons = np.zeros(len(coord_region))
for i in range(len(coord_region)):
lats[i] = coord_region[i][0]
lons[i] = coord_region[i][1]
uppergp = []
lowergp = []
for i in range(len(coord_region)-1):
if lats[i] < lats[i+1]:
uppergp.append( [lats[i], lons[i]] )
lowergp.append( [lats[i+1], lons[i+1]] )
uppergp.append( [lats[-1], lons[-1]] )
lowergp.insert(0, [lats[0], lons[0]] )
lowergp.reverse()
boundgp = uppergp + lowergp
vertlist = []
for i in range(len(boundgp)):
vertlist.append( (longitude[int(boundgp[i][1])]+1.125/2., latitude[int(boundgp[i][0])]-1.125/2.))
verts = vertlist
# adding last vert to list to close poly
verts.append(verts[-1])
Path = mpath.Path
lineto = Path.LINETO
codes = [Path.MOVETO, Path.CLOSEPOLY]
for i in range(len(boundgp)-1):
codes.insert(1, lineto)
boundgpcoord = mpath.Path(verts, codes)
return boundgpcoord, Region
}

Categories