From 9db7a4d3d4513f30d46b08f50c7637a505db7799 Mon Sep 17 00:00:00 2001 From: Magnus Ulimoen Date: Tue, 31 Dec 2024 11:56:47 +0100 Subject: [PATCH 1/5] Fix contour_to_geojson --- geojsoncontour/contour.py | 34 ++++++++++++++---------- geojsoncontour/utilities/vertices.py | 39 ++++++++++++++++++++++++++++ tests/benchmark_test1.geojson | 2 +- 3 files changed, 61 insertions(+), 14 deletions(-) create mode 100644 geojsoncontour/utilities/vertices.py diff --git a/geojsoncontour/contour.py b/geojsoncontour/contour.py index 6bb7afa..d676869 100644 --- a/geojsoncontour/contour.py +++ b/geojsoncontour/contour.py @@ -5,36 +5,44 @@ from matplotlib.colors import rgb2hex from geojson import Feature, LineString from geojson import Polygon, FeatureCollection + from .utilities.multipoly import MP, keep_high_angle, set_contourf_properties,get_contourf_levels +from .utilities.vertices import get_vertices_from_path def contour_to_geojson(contour, geojson_filepath=None, min_angle_deg=None, ndigits=5, unit='', stroke_width=1, geojson_properties=None, strdump=False, serialize=True): """Transform matplotlib.contour to geojson.""" - collections = contour.collections - contour_index = 0 + # collections = contour.collections line_features = [] - for collection in collections: - color = collection.get_edgecolor() - for path in collection.get_paths(): - v = path.vertices - if len(v) < 3: + paths = contour.get_paths() + colors = contour.get_edgecolors() + levels = contour.levels + for contour_index, (path, color, level) in enumerate(zip(paths, colors, levels)): + for coordinates in get_vertices_from_path(path): + if len(coordinates) < 3: continue - coordinates = keep_high_angle(v, min_angle_deg) if min_angle_deg else v - coordinates = np.around(coordinates, ndigits) if ndigits is not None else coordinates + if np.all(np.equal(coordinates, coordinates[0])): + # Matplotlib sometimes emits empty paths which + # can be ignored + continue + if min_angle_deg: + coordinates = keep_high_angle(coordinates, min_angle_deg) + if ndigits: + coordinates = np.around(coordinates, ndigits) line = LineString(coordinates.tolist()) properties = { "stroke-width": stroke_width, - "stroke": rgb2hex(color[0]), - "title": "%.2f" % contour.levels[contour_index] + ' ' + unit, - "level-value": float("%.6f" % contour.levels[contour_index]), + "stroke": rgb2hex(color), + "title": f"{level:.2f} {unit}", + "level-value": float(f"{level:.6f}"), "level-index": contour_index } if geojson_properties: properties.update(geojson_properties) line_features.append(Feature(geometry=line, properties=properties)) - contour_index += 1 + feature_collection = FeatureCollection(line_features) return _render_feature_collection(feature_collection, geojson_filepath, strdump, serialize) diff --git a/geojsoncontour/utilities/vertices.py b/geojsoncontour/utilities/vertices.py new file mode 100644 index 0000000..81a9dc8 --- /dev/null +++ b/geojsoncontour/utilities/vertices.py @@ -0,0 +1,39 @@ +from collections.abc import Generator + +import numpy as np +import numpy.typing as npt +from matplotlib.path import Path + +def get_vertices_from_path(path: Path) -> Generator[npt.NDArray[float], None, None]: + """Splits the vertices from path into continous lines, + by taking into account path.codes + + See https://matplotlib.org/stable/api/path_api.html for a + description of path.vertices/path.codes + """ + # path = path.cleaned(curves=False, simplify=False) + vertices = path.vertices + codes = path.codes + if codes is None: + codes = [Path.MOVETO] + [Path.LINETO]*(len(vertices)-1) + current_vertice = [] + for (v, c) in zip(vertices, codes): + if c == Path.STOP: + if len(current_vertice) != 0: + yield np.array(current_vertice) + current_vertice = [] + elif c == Path.MOVETO: + if len(current_vertice) != 0: + yield np.array(current_vertice) + current_vertice = [v] + elif c == Path.LINETO: + current_vertice.append(v) + elif c == Path.CLOSEPOLY: + if len(current_vertice) != 0: + current_vertice.append(current_vertice[0]) + yield np.array(current_vertice) + current_vertice = [] + else: + raise Exception(f"Unknown code {c} encountered") + if len(current_vertice) != 0: + yield np.array(current_vertice) diff --git a/tests/benchmark_test1.geojson b/tests/benchmark_test1.geojson index c24873d..21abacb 100644 --- a/tests/benchmark_test1.geojson +++ b/tests/benchmark_test1.geojson @@ -1 +1 @@ -{"features":[{"geometry":{"coordinates":[[-4.0,-22.085],[-10.18,-20.0],[-15.0,-16.693],[-19.0,-11.946],[-21.627,-6.0],[-22.444,0.0],[-21.627,6.0],[-19.563,11.0],[-16.0,15.737],[-11.946,19.0],[-6.0,21.627],[0.0,22.444],[6.0,21.627],[11.0,19.563],[15.737,16.0],[19.0,11.946],[21.627,6.0],[22.444,0.0],[21.627,-6.0],[19.563,-11.0],[16.0,-15.737],[11.946,-19.0],[6.0,-21.627],[0.0,-22.444],[-4.0,-22.085]],"type":"LineString"},"properties":{"level-index":0,"level-value":22.444444,"stroke":"#000080","stroke-width":5,"title":"22.44 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-8.0,-44.17],[-20.0,-40.187],[-29.307,-34.0],[-37.282,-25.0],[-42.65,-14.0],[-44.789,-3.0],[-44.0,8.883],[-40.187,20.0],[-34.0,29.307],[-25.0,37.282],[-14.0,42.65],[-3.0,44.789],[8.883,44.0],[20.0,40.187],[29.307,34.0],[37.282,25.0],[42.65,14.0],[44.789,3.0],[44.0,-8.883],[40.187,-20.0],[34.0,-29.307],[25.0,-37.282],[14.0,-42.65],[3.0,-44.789],[-8.0,-44.17]],"type":"LineString"},"properties":{"level-index":1,"level-value":44.888889,"stroke":"#0010ff","stroke-width":5,"title":"44.89 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-6.0,-67.065],[-24.0,-62.911],[-39.0,-54.888],[-52.0,-42.775],[-61.235,-28.0],[-66.429,-11.0],[-67.0,6.675],[-62.911,24.0],[-54.888,39.0],[-42.775,52.0],[-28.0,61.235],[-11.0,66.429],[6.675,67.0],[24.0,62.911],[39.0,54.888],[52.0,42.775],[61.235,28.0],[66.429,11.0],[67.0,-6.675],[62.911,-24.0],[54.888,-39.0],[42.775,-52.0],[28.0,-61.235],[11.0,-66.429],[-6.0,-67.065]],"type":"LineString"},"properties":{"level-index":2,"level-value":67.333333,"stroke":"#00a4ff","stroke-width":5,"title":"67.33 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[11.785,89.0],[34.218,83.0],[55.0,70.958],[71.722,54.0],[83.091,34.0],[89.0,11.785],[88.972,-12.0],[83.0,-34.218],[70.958,-55.0],[54.0,-71.722],[34.0,-83.091],[11.785,-89.0],[-12.0,-88.972],[-34.218,-83.0],[-55.0,-70.958],[-71.722,-54.0],[-83.091,-34.0],[-89.0,-11.785],[-88.972,12.0],[-83.0,34.218],[-70.958,55.0],[-54.0,71.722],[-34.0,83.091],[-11.785,89.0]],"type":"LineString"},"properties":{"level-index":3,"level-value":89.777778,"stroke":"#40ffb7","stroke-width":5,"title":"89.78 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-67.036,-90.0],[-88.503,-69.0],[-103.237,-44.0],[-111.076,-16.0],[-111.467,13.0],[-104.464,41.0],[-90.762,66.0],[-70.886,87.0],[-68.357,89.0]],"type":"LineString"},"properties":{"level-index":4,"level-value":112.222222,"stroke":"#b7ff40","stroke-width":5,"title":"112.22 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[68.357,89.0],[89.273,68.0],[104.0,42.163],[111.346,14.0],[111.215,-15.0],[103.657,-43.0],[89.0,-68.357],[67.036,-90.0]],"type":"LineString"},"properties":{"level-index":4,"level-value":112.222222,"stroke":"#b7ff40","stroke-width":5,"title":"112.22 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-100.175,-90.0],[-120.561,-60.0],[-131.932,-27.0],[-134.429,8.0],[-127.95,42.0],[-113.0,73.253],[-101.065,89.0]],"type":"LineString"},"properties":{"level-index":5,"level-value":134.666667,"stroke":"#ffb900","stroke-width":5,"title":"134.67 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[101.065,89.0],[121.054,59.0],[132.133,26.0],[134.366,-9.0],[127.617,-43.0],[112.0,-74.773],[100.175,-90.0]],"type":"LineString"},"properties":{"level-index":5,"level-value":134.666667,"stroke":"#ffb900","stroke-width":5,"title":"134.67 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-128.778,-90.0],[-147.902,-53.0],[-156.572,-13.0],[-154.596,28.0],[-142.109,67.0],[-129.471,89.0]],"type":"LineString"},"properties":{"level-index":6,"level-value":157.111111,"stroke":"#ff3000","stroke-width":5,"title":"157.11 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[129.471,89.0],[148.256,52.0],[156.652,12.0],[154.411,-29.0],[141.633,-68.0],[128.778,-90.0]],"type":"LineString"},"properties":{"level-index":6,"level-value":157.111111,"stroke":"#ff3000","stroke-width":5,"title":"157.11 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-155.371,-90.0],[-173.563,-46.0],[-179.556,0.0],[-173.563,46.0],[-155.946,89.0]],"type":"LineString"},"properties":{"level-index":7,"level-value":179.555556,"stroke":"#800000","stroke-width":5,"title":"179.56 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[179.0,-14.11],[169.0,-60.655],[155.371,-90.0]],"type":"LineString"},"properties":{"level-index":7,"level-value":179.555556,"stroke":"#800000","stroke-width":5,"title":"179.56 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[155.946,89.0],[173.825,45.0],[179.0,14.11]],"type":"LineString"},"properties":{"level-index":7,"level-value":179.555556,"stroke":"#800000","stroke-width":5,"title":"179.56 [unit]"},"type":"Feature"}],"type":"FeatureCollection"} \ No newline at end of file +{"features":[{"geometry":{"coordinates":[[-4.0,-22.085],[-10.18,-20.0],[-15.0,-16.693],[-19.0,-11.946],[-21.627,-6.0],[-22.444,0.0],[-21.627,6.0],[-19.563,11.0],[-16.0,15.737],[-11.946,19.0],[-6.0,21.627],[0.0,22.444],[6.0,21.627],[11.0,19.563],[15.737,16.0],[19.0,11.946],[21.627,6.0],[22.444,0.0],[21.627,-6.0],[19.563,-11.0],[16.0,-15.737],[11.946,-19.0],[6.0,-21.627],[0.0,-22.444],[-4.0,-22.085]],"type":"LineString"},"properties":{"level-index":1,"level-value":22.444444,"stroke":"#0000ff","stroke-width":5,"title":"22.44 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-8.0,-44.17],[-20.0,-40.187],[-29.307,-34.0],[-37.282,-25.0],[-42.65,-14.0],[-44.789,-3.0],[-44.0,8.883],[-40.187,20.0],[-34.0,29.307],[-25.0,37.282],[-14.0,42.65],[-3.0,44.789],[8.883,44.0],[20.0,40.187],[29.307,34.0],[37.282,25.0],[42.65,14.0],[44.789,3.0],[44.0,-8.883],[40.187,-20.0],[34.0,-29.307],[25.0,-37.282],[14.0,-42.65],[3.0,-44.789],[-8.0,-44.17]],"type":"LineString"},"properties":{"level-index":2,"level-value":44.888889,"stroke":"#0060ff","stroke-width":5,"title":"44.89 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-6.0,-67.065],[-24.0,-62.911],[-39.0,-54.888],[-52.0,-42.775],[-61.235,-28.0],[-66.429,-11.0],[-67.0,6.675],[-62.911,24.0],[-54.888,39.0],[-42.775,52.0],[-28.0,61.235],[-11.0,66.429],[6.675,67.0],[24.0,62.911],[39.0,54.888],[52.0,42.775],[61.235,28.0],[66.429,11.0],[67.0,-6.675],[62.911,-24.0],[54.888,-39.0],[42.775,-52.0],[28.0,-61.235],[11.0,-66.429],[-6.0,-67.065]],"type":"LineString"},"properties":{"level-index":3,"level-value":67.333333,"stroke":"#00d4ff","stroke-width":5,"title":"67.33 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[11.785,89.0],[34.218,83.0],[55.0,70.958],[71.722,54.0],[83.091,34.0],[89.0,11.785],[88.972,-12.0],[83.0,-34.218],[70.958,-55.0],[54.0,-71.722],[34.0,-83.091],[11.785,-89.0],[-12.0,-88.972],[-34.218,-83.0],[-55.0,-70.958],[-71.722,-54.0],[-83.091,-34.0],[-89.0,-11.785],[-88.972,12.0],[-83.0,34.218],[-70.958,55.0],[-54.0,71.722],[-34.0,83.091],[-11.785,89.0]],"type":"LineString"},"properties":{"level-index":4,"level-value":89.777778,"stroke":"#4dffaa","stroke-width":5,"title":"89.78 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-67.036,-90.0],[-88.503,-69.0],[-103.237,-44.0],[-111.076,-16.0],[-111.467,13.0],[-104.464,41.0],[-90.762,66.0],[-70.886,87.0],[-68.357,89.0]],"type":"LineString"},"properties":{"level-index":5,"level-value":112.222222,"stroke":"#aaff4d","stroke-width":5,"title":"112.22 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[68.357,89.0],[89.273,68.0],[104.0,42.163],[111.346,14.0],[111.215,-15.0],[103.657,-43.0],[89.0,-68.357],[67.036,-90.0]],"type":"LineString"},"properties":{"level-index":5,"level-value":112.222222,"stroke":"#aaff4d","stroke-width":5,"title":"112.22 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-100.175,-90.0],[-120.561,-60.0],[-131.932,-27.0],[-134.429,8.0],[-127.95,42.0],[-113.0,73.253],[-101.065,89.0]],"type":"LineString"},"properties":{"level-index":6,"level-value":134.666667,"stroke":"#ffe600","stroke-width":5,"title":"134.67 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[101.065,89.0],[121.054,59.0],[132.133,26.0],[134.366,-9.0],[127.617,-43.0],[112.0,-74.773],[100.175,-90.0]],"type":"LineString"},"properties":{"level-index":6,"level-value":134.666667,"stroke":"#ffe600","stroke-width":5,"title":"134.67 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-128.778,-90.0],[-147.902,-53.0],[-156.572,-13.0],[-154.596,28.0],[-142.109,67.0],[-129.471,89.0]],"type":"LineString"},"properties":{"level-index":7,"level-value":157.111111,"stroke":"#ff7a00","stroke-width":5,"title":"157.11 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[129.471,89.0],[148.256,52.0],[156.652,12.0],[154.411,-29.0],[141.633,-68.0],[128.778,-90.0]],"type":"LineString"},"properties":{"level-index":7,"level-value":157.111111,"stroke":"#ff7a00","stroke-width":5,"title":"157.11 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-155.371,-90.0],[-173.563,-46.0],[-179.556,0.0],[-173.563,46.0],[-155.946,89.0]],"type":"LineString"},"properties":{"level-index":8,"level-value":179.555556,"stroke":"#ff1300","stroke-width":5,"title":"179.56 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[179.0,-14.11],[169.0,-60.655],[155.371,-90.0]],"type":"LineString"},"properties":{"level-index":8,"level-value":179.555556,"stroke":"#ff1300","stroke-width":5,"title":"179.56 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[155.946,89.0],[173.825,45.0],[179.0,14.11]],"type":"LineString"},"properties":{"level-index":8,"level-value":179.555556,"stroke":"#ff1300","stroke-width":5,"title":"179.56 [unit]"},"type":"Feature"}],"type":"FeatureCollection"} \ No newline at end of file From 53b5061cc14cb7f72bbae98d97a2296b19adab78 Mon Sep 17 00:00:00 2001 From: Magnus Ulimoen Date: Tue, 31 Dec 2024 11:56:47 +0100 Subject: [PATCH 2/5] Fix contour_to_geojson --- geojsoncontour/contour.py | 1 - tests/benchmark_test_properties.geojson | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/geojsoncontour/contour.py b/geojsoncontour/contour.py index d676869..5f63a89 100644 --- a/geojsoncontour/contour.py +++ b/geojsoncontour/contour.py @@ -14,7 +14,6 @@ def contour_to_geojson(contour, geojson_filepath=None, min_angle_deg=None, ndigits=5, unit='', stroke_width=1, geojson_properties=None, strdump=False, serialize=True): """Transform matplotlib.contour to geojson.""" - # collections = contour.collections line_features = [] paths = contour.get_paths() colors = contour.get_edgecolors() diff --git a/tests/benchmark_test_properties.geojson b/tests/benchmark_test_properties.geojson index f7efa8d..f80a412 100644 --- a/tests/benchmark_test_properties.geojson +++ b/tests/benchmark_test_properties.geojson @@ -1 +1 @@ -{"features":[{"geometry":{"coordinates":[[-4.0,-22.085],[-10.18,-20.0],[-15.0,-16.693],[-19.0,-11.946],[-21.627,-6.0],[-22.444,0.0],[-21.627,6.0],[-19.563,11.0],[-16.0,15.737],[-11.946,19.0],[-6.0,21.627],[0.0,22.444],[6.0,21.627],[11.0,19.563],[15.737,16.0],[19.0,11.946],[21.627,6.0],[22.444,0.0],[21.627,-6.0],[19.563,-11.0],[16.0,-15.737],[11.946,-19.0],[6.0,-21.627],[0.0,-22.444],[-4.0,-22.085]],"type":"LineString"},"properties":{"description":"A description","level-index":0,"level-value":22.444444,"stroke":"#000080","stroke-opacity":1.0,"stroke-width":5,"title":"22.44 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-8.0,-44.17],[-20.0,-40.187],[-29.307,-34.0],[-37.282,-25.0],[-42.65,-14.0],[-44.789,-3.0],[-44.0,8.883],[-40.187,20.0],[-34.0,29.307],[-25.0,37.282],[-14.0,42.65],[-3.0,44.789],[8.883,44.0],[20.0,40.187],[29.307,34.0],[37.282,25.0],[42.65,14.0],[44.789,3.0],[44.0,-8.883],[40.187,-20.0],[34.0,-29.307],[25.0,-37.282],[14.0,-42.65],[3.0,-44.789],[-8.0,-44.17]],"type":"LineString"},"properties":{"description":"A description","level-index":1,"level-value":44.888889,"stroke":"#0010ff","stroke-opacity":1.0,"stroke-width":5,"title":"44.89 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-6.0,-67.065],[-24.0,-62.911],[-39.0,-54.888],[-52.0,-42.775],[-61.235,-28.0],[-66.429,-11.0],[-67.0,6.675],[-62.911,24.0],[-54.888,39.0],[-42.775,52.0],[-28.0,61.235],[-11.0,66.429],[6.675,67.0],[24.0,62.911],[39.0,54.888],[52.0,42.775],[61.235,28.0],[66.429,11.0],[67.0,-6.675],[62.911,-24.0],[54.888,-39.0],[42.775,-52.0],[28.0,-61.235],[11.0,-66.429],[-6.0,-67.065]],"type":"LineString"},"properties":{"description":"A description","level-index":2,"level-value":67.333333,"stroke":"#00a4ff","stroke-opacity":1.0,"stroke-width":5,"title":"67.33 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[11.785,89.0],[34.218,83.0],[55.0,70.958],[71.722,54.0],[83.091,34.0],[89.0,11.785],[88.972,-12.0],[83.0,-34.218],[70.958,-55.0],[54.0,-71.722],[34.0,-83.091],[11.785,-89.0],[-12.0,-88.972],[-34.218,-83.0],[-55.0,-70.958],[-71.722,-54.0],[-83.091,-34.0],[-89.0,-11.785],[-88.972,12.0],[-83.0,34.218],[-70.958,55.0],[-54.0,71.722],[-34.0,83.091],[-11.785,89.0]],"type":"LineString"},"properties":{"description":"A description","level-index":3,"level-value":89.777778,"stroke":"#40ffb7","stroke-opacity":1.0,"stroke-width":5,"title":"89.78 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-67.036,-90.0],[-88.503,-69.0],[-103.237,-44.0],[-111.076,-16.0],[-111.467,13.0],[-104.464,41.0],[-90.762,66.0],[-70.886,87.0],[-68.357,89.0]],"type":"LineString"},"properties":{"description":"A description","level-index":4,"level-value":112.222222,"stroke":"#b7ff40","stroke-opacity":1.0,"stroke-width":5,"title":"112.22 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[68.357,89.0],[89.273,68.0],[104.0,42.163],[111.346,14.0],[111.215,-15.0],[103.657,-43.0],[89.0,-68.357],[67.036,-90.0]],"type":"LineString"},"properties":{"description":"A description","level-index":4,"level-value":112.222222,"stroke":"#b7ff40","stroke-opacity":1.0,"stroke-width":5,"title":"112.22 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-100.175,-90.0],[-120.561,-60.0],[-131.932,-27.0],[-134.429,8.0],[-127.95,42.0],[-113.0,73.253],[-101.065,89.0]],"type":"LineString"},"properties":{"description":"A description","level-index":5,"level-value":134.666667,"stroke":"#ffb900","stroke-opacity":1.0,"stroke-width":5,"title":"134.67 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[101.065,89.0],[121.054,59.0],[132.133,26.0],[134.366,-9.0],[127.617,-43.0],[112.0,-74.773],[100.175,-90.0]],"type":"LineString"},"properties":{"description":"A description","level-index":5,"level-value":134.666667,"stroke":"#ffb900","stroke-opacity":1.0,"stroke-width":5,"title":"134.67 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-128.778,-90.0],[-147.902,-53.0],[-156.572,-13.0],[-154.596,28.0],[-142.109,67.0],[-129.471,89.0]],"type":"LineString"},"properties":{"description":"A description","level-index":6,"level-value":157.111111,"stroke":"#ff3000","stroke-opacity":1.0,"stroke-width":5,"title":"157.11 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[129.471,89.0],[148.256,52.0],[156.652,12.0],[154.411,-29.0],[141.633,-68.0],[128.778,-90.0]],"type":"LineString"},"properties":{"description":"A description","level-index":6,"level-value":157.111111,"stroke":"#ff3000","stroke-opacity":1.0,"stroke-width":5,"title":"157.11 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-155.371,-90.0],[-173.563,-46.0],[-179.556,0.0],[-173.563,46.0],[-155.946,89.0]],"type":"LineString"},"properties":{"description":"A description","level-index":7,"level-value":179.555556,"stroke":"#800000","stroke-opacity":1.0,"stroke-width":5,"title":"179.56 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[179.0,-14.11],[169.0,-60.655],[155.371,-90.0]],"type":"LineString"},"properties":{"description":"A description","level-index":7,"level-value":179.555556,"stroke":"#800000","stroke-opacity":1.0,"stroke-width":5,"title":"179.56 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[155.946,89.0],[173.825,45.0],[179.0,14.11]],"type":"LineString"},"properties":{"description":"A description","level-index":7,"level-value":179.555556,"stroke":"#800000","stroke-opacity":1.0,"stroke-width":5,"title":"179.56 [unit]"},"type":"Feature"}],"type":"FeatureCollection"} \ No newline at end of file +{"features":[{"geometry":{"coordinates":[[-4.0,-22.085],[-10.18,-20.0],[-15.0,-16.693],[-19.0,-11.946],[-21.627,-6.0],[-22.444,0.0],[-21.627,6.0],[-19.563,11.0],[-16.0,15.737],[-11.946,19.0],[-6.0,21.627],[0.0,22.444],[6.0,21.627],[11.0,19.563],[15.737,16.0],[19.0,11.946],[21.627,6.0],[22.444,0.0],[21.627,-6.0],[19.563,-11.0],[16.0,-15.737],[11.946,-19.0],[6.0,-21.627],[0.0,-22.444],[-4.0,-22.085]],"type":"LineString"},"properties":{"description":"A description","level-index":1,"level-value":22.444444,"stroke":"#0000ff","stroke-opacity":1.0,"stroke-width":5,"title":"22.44 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-8.0,-44.17],[-20.0,-40.187],[-29.307,-34.0],[-37.282,-25.0],[-42.65,-14.0],[-44.789,-3.0],[-44.0,8.883],[-40.187,20.0],[-34.0,29.307],[-25.0,37.282],[-14.0,42.65],[-3.0,44.789],[8.883,44.0],[20.0,40.187],[29.307,34.0],[37.282,25.0],[42.65,14.0],[44.789,3.0],[44.0,-8.883],[40.187,-20.0],[34.0,-29.307],[25.0,-37.282],[14.0,-42.65],[3.0,-44.789],[-8.0,-44.17]],"type":"LineString"},"properties":{"description":"A description","level-index":2,"level-value":44.888889,"stroke":"#0060ff","stroke-opacity":1.0,"stroke-width":5,"title":"44.89 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-6.0,-67.065],[-24.0,-62.911],[-39.0,-54.888],[-52.0,-42.775],[-61.235,-28.0],[-66.429,-11.0],[-67.0,6.675],[-62.911,24.0],[-54.888,39.0],[-42.775,52.0],[-28.0,61.235],[-11.0,66.429],[6.675,67.0],[24.0,62.911],[39.0,54.888],[52.0,42.775],[61.235,28.0],[66.429,11.0],[67.0,-6.675],[62.911,-24.0],[54.888,-39.0],[42.775,-52.0],[28.0,-61.235],[11.0,-66.429],[-6.0,-67.065]],"type":"LineString"},"properties":{"description":"A description","level-index":3,"level-value":67.333333,"stroke":"#00d4ff","stroke-opacity":1.0,"stroke-width":5,"title":"67.33 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[11.785,89.0],[34.218,83.0],[55.0,70.958],[71.722,54.0],[83.091,34.0],[89.0,11.785],[88.972,-12.0],[83.0,-34.218],[70.958,-55.0],[54.0,-71.722],[34.0,-83.091],[11.785,-89.0],[-12.0,-88.972],[-34.218,-83.0],[-55.0,-70.958],[-71.722,-54.0],[-83.091,-34.0],[-89.0,-11.785],[-88.972,12.0],[-83.0,34.218],[-70.958,55.0],[-54.0,71.722],[-34.0,83.091],[-11.785,89.0]],"type":"LineString"},"properties":{"description":"A description","level-index":4,"level-value":89.777778,"stroke":"#4dffaa","stroke-opacity":1.0,"stroke-width":5,"title":"89.78 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-67.036,-90.0],[-88.503,-69.0],[-103.237,-44.0],[-111.076,-16.0],[-111.467,13.0],[-104.464,41.0],[-90.762,66.0],[-70.886,87.0],[-68.357,89.0]],"type":"LineString"},"properties":{"description":"A description","level-index":5,"level-value":112.222222,"stroke":"#aaff4d","stroke-opacity":1.0,"stroke-width":5,"title":"112.22 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[68.357,89.0],[89.273,68.0],[104.0,42.163],[111.346,14.0],[111.215,-15.0],[103.657,-43.0],[89.0,-68.357],[67.036,-90.0]],"type":"LineString"},"properties":{"description":"A description","level-index":5,"level-value":112.222222,"stroke":"#aaff4d","stroke-opacity":1.0,"stroke-width":5,"title":"112.22 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-100.175,-90.0],[-120.561,-60.0],[-131.932,-27.0],[-134.429,8.0],[-127.95,42.0],[-113.0,73.253],[-101.065,89.0]],"type":"LineString"},"properties":{"description":"A description","level-index":6,"level-value":134.666667,"stroke":"#ffe600","stroke-opacity":1.0,"stroke-width":5,"title":"134.67 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[101.065,89.0],[121.054,59.0],[132.133,26.0],[134.366,-9.0],[127.617,-43.0],[112.0,-74.773],[100.175,-90.0]],"type":"LineString"},"properties":{"description":"A description","level-index":6,"level-value":134.666667,"stroke":"#ffe600","stroke-opacity":1.0,"stroke-width":5,"title":"134.67 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-128.778,-90.0],[-147.902,-53.0],[-156.572,-13.0],[-154.596,28.0],[-142.109,67.0],[-129.471,89.0]],"type":"LineString"},"properties":{"description":"A description","level-index":7,"level-value":157.111111,"stroke":"#ff7a00","stroke-opacity":1.0,"stroke-width":5,"title":"157.11 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[129.471,89.0],[148.256,52.0],[156.652,12.0],[154.411,-29.0],[141.633,-68.0],[128.778,-90.0]],"type":"LineString"},"properties":{"description":"A description","level-index":7,"level-value":157.111111,"stroke":"#ff7a00","stroke-opacity":1.0,"stroke-width":5,"title":"157.11 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[-155.371,-90.0],[-173.563,-46.0],[-179.556,0.0],[-173.563,46.0],[-155.946,89.0]],"type":"LineString"},"properties":{"description":"A description","level-index":8,"level-value":179.555556,"stroke":"#ff1300","stroke-opacity":1.0,"stroke-width":5,"title":"179.56 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[179.0,-14.11],[169.0,-60.655],[155.371,-90.0]],"type":"LineString"},"properties":{"description":"A description","level-index":8,"level-value":179.555556,"stroke":"#ff1300","stroke-opacity":1.0,"stroke-width":5,"title":"179.56 [unit]"},"type":"Feature"},{"geometry":{"coordinates":[[155.946,89.0],[173.825,45.0],[179.0,14.11]],"type":"LineString"},"properties":{"description":"A description","level-index":8,"level-value":179.555556,"stroke":"#ff1300","stroke-opacity":1.0,"stroke-width":5,"title":"179.56 [unit]"},"type":"Feature"}],"type":"FeatureCollection"} \ No newline at end of file From 65105514b3f65b474662630f7dc77d4a59fd2c6d Mon Sep 17 00:00:00 2001 From: Magnus Ulimoen Date: Tue, 31 Dec 2024 12:00:38 +0100 Subject: [PATCH 3/5] Bump minimum matplotlib --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 535cc72..e58ec7d 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ install_requires=[ 'geojson', 'numpy', - 'matplotlib', + 'matplotlib>=3.8', 'xarray' ], zip_safe=False, From 1623466f6ebf46e38e912e1e36e619335dd56276 Mon Sep 17 00:00:00 2001 From: Magnus Ulimoen Date: Tue, 31 Dec 2024 13:13:37 +0100 Subject: [PATCH 4/5] Fix contourf plotting --- geojsoncontour/contour.py | 30 ++++++------ geojsoncontour/utilities/multipoly.py | 67 +++++++++++++++++++-------- 2 files changed, 63 insertions(+), 34 deletions(-) diff --git a/geojsoncontour/contour.py b/geojsoncontour/contour.py index 5f63a89..599453c 100644 --- a/geojsoncontour/contour.py +++ b/geojsoncontour/contour.py @@ -6,7 +6,7 @@ from geojson import Feature, LineString from geojson import Polygon, FeatureCollection -from .utilities.multipoly import MP, keep_high_angle, set_contourf_properties,get_contourf_levels +from .utilities.multipoly import multi_polygon, keep_high_angle, set_contourf_properties,get_contourf_levels from .utilities.vertices import get_vertices_from_path @@ -85,20 +85,20 @@ def contourf_to_geojson(contourf, geojson_filepath=None, min_angle_deg=None, variable_opacity = False polygon_features = [] contourf_levels = get_contourf_levels(contourf.levels, contourf.extend) - for coll, level in zip(contourf.collections, contourf_levels): - color = coll.get_facecolor() - muli = MP(coll, min_angle_deg, ndigits) - polygon = muli.mpoly() - fcolor = rgb2hex(color[0]) - if polygon.coordinates: - properties = set_contourf_properties(stroke_width, fcolor, fill_opacity, level, unit) - if geojson_properties: - properties.update(geojson_properties) - feature = Feature(geometry=polygon, properties=properties) - polygon_features.append(feature) - # print(len(polygon.coordinates)) - if variable_opacity: - fill_opacity += opacity_increment + contourf_colors = contourf.get_facecolor() + for path, level, color in zip(contourf.get_paths(), contourf_levels, contourf_colors): + polygon = multi_polygon(path, min_angle_deg, ndigits) + if not polygon.coordinates: + continue + fcolor = rgb2hex(color) + properties = set_contourf_properties(stroke_width, fcolor, fill_opacity, level, unit) + if geojson_properties: + properties.update(geojson_properties) + feature = Feature(geometry=polygon, properties=properties) + polygon_features.append(feature) + # print(len(polygon.coordinates)) + if variable_opacity: + fill_opacity += opacity_increment feature_collection = FeatureCollection(polygon_features) return _render_feature_collection(feature_collection, geojson_filepath, strdump, serialize) diff --git a/geojsoncontour/utilities/multipoly.py b/geojsoncontour/utilities/multipoly.py index 42b1803..961a2ab 100644 --- a/geojsoncontour/utilities/multipoly.py +++ b/geojsoncontour/utilities/multipoly.py @@ -1,32 +1,61 @@ #!/usr/bin/python3.4 # -*- encoding: utf-8 -*- """Helper module for transformation of matplotlib.contour(f) to GeoJSON.""" +import enum + from geojson import MultiPolygon import numpy as np +from .vertices import get_vertices_from_path + -class MP(object): - """Class for easy MultiPolygon generation. +class Orientation(enum.IntEnum): + CW = enum.auto() + CCW = enum.auto() + - This class converts a matplotlib PathCollection into a GeoJSON MultiPolygon. +def orientation(vertices) -> Orientation: + """Determine orientation for a closed polygon + + See https://en.wikipedia.org/wiki/Curve_orientation for an + explanation of this formula """ + # Ignoring last index (which is duplicate of first) + vertices = vertices[:-1, :] + lowest_x_indices = np.flatnonzero(vertices[:, 0] == np.min(vertices[:, 0])) + lowest_y_indices = np.flatnonzero(vertices[lowest_x_indices, 1] == np.min(vertices[lowest_x_indices, 1])) + index = lowest_x_indices[lowest_y_indices[0]] + + prev_index = index - 1 + next_index = (index + 1) % vertices.shape[0] + xa, ya = vertices[prev_index, :] + xb, yb = vertices[index, :] + xc, yc = vertices[next_index, :] + + detO = (xb - xa)*(yc - ya) - (xc - xa)*(yb - ya) + if detO > 0: + return Orientation.CCW + else: + return Orientation.CW + + +def multi_polygon(path, min_angle_deg, ndigits): + polygons = [] + for linestring in path.to_polygons(): + if min_angle_deg: + linestring = keep_high_angle(linestring, min_angle_deg) + if ndigits: + linestring = np.around(linestring, ndigits) + + handedness = orientation(linestring) + if handedness == Orientation.CCW: + polygons.append([linestring.tolist()]) + else: + # This is a hole, which we assume belong + # to the previous polygon + polygons[-1].extend([linestring.tolist()]) - def __init__(self, path_collection, min_angle_deg, ndigits): - self.coords = [] - for path in path_collection.get_paths(): - polygon = [] - for linestring in path.to_polygons(): - if min_angle_deg: - linestring = keep_high_angle(linestring, min_angle_deg) - if ndigits: - linestring = np.around(linestring, ndigits) - polygon.append(linestring.tolist()) - if polygon: - self.coords.append(polygon) - - def mpoly(self): - """Output of GeoJSON MultiPolygon object.""" - return MultiPolygon(coordinates=self.coords) + return MultiPolygon(coordinates=polygons) def unit_vector(vector): From 14257228fd97d6d563a46341cf612fa36ff47727 Mon Sep 17 00:00:00 2001 From: Magnus Ulimoen Date: Thu, 2 Jan 2025 12:49:42 +0100 Subject: [PATCH 5/5] Fix geojson_overlap --- geojsoncontour/contour.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/geojsoncontour/contour.py b/geojsoncontour/contour.py index 599453c..7d95153 100644 --- a/geojsoncontour/contour.py +++ b/geojsoncontour/contour.py @@ -51,23 +51,21 @@ def contourf_to_geojson_overlap(contourf, geojson_filepath=None, min_angle_deg=N geojson_properties=None, strdump=False, serialize=True): """Transform matplotlib.contourf to geojson with overlapping filled contours.""" polygon_features = [] - contourf_idx = 0 contourf_levels = get_contourf_levels(contourf.levels, contourf.extend) - for collection in contourf.collections: - color = collection.get_facecolor() - for path in collection.get_paths(): - for coord in path.to_polygons(): - if min_angle_deg: - coord = keep_high_angle(coord, min_angle_deg) - coord = np.around(coord, ndigits) if ndigits else coord - polygon = Polygon(coordinates=[coord.tolist()]) - fcolor = rgb2hex(color[0]) - properties = set_contourf_properties(stroke_width, fcolor, fill_opacity, contourf_levels[contourf_idx], unit) - if geojson_properties: - properties.update(geojson_properties) - feature = Feature(geometry=polygon, properties=properties) - polygon_features.append(feature) - contourf_idx += 1 + contourf_colors = contourf.get_facecolor() + for path, level, color in zip(contourf.get_paths(), contourf_levels, contourf_colors): + for coord in get_vertices_from_path(path): + if min_angle_deg: + coord = keep_high_angle(coord, min_angle_deg) + if ndigits: + coord = np.around(coord, ndigits) + polygon = Polygon(coordinates=[coord.tolist()]) + fcolor = rgb2hex(color) + properties = set_contourf_properties(stroke_width, fcolor, fill_opacity, level, unit) + if geojson_properties: + properties.update(geojson_properties) + feature = Feature(geometry=polygon, properties=properties) + polygon_features.append(feature) feature_collection = FeatureCollection(polygon_features) return _render_feature_collection(feature_collection, geojson_filepath, strdump, serialize)