Commit edb0a4a4 authored by Matthias Betz's avatar Matthias Betz
Browse files

Merge branch 'master' of transfer.hft-stuttgart.de:circulargreensimcity/circulargreensimcity

parents 0b3a4414 0842edf1
......@@ -2,6 +2,9 @@
# Created by https://www.gitignore.io/api/java,maven,macos,linux,eclipse,windows,netbeans,intellij
# Edit at https://www.gitignore.io/?templates=java,maven,macos,linux,eclipse,windows,netbeans,intellij
data/Marieke
data/*.gml
# User specific
.sonarlint/
......
Name
 .
├──  75Bäume
│ ├──  Baum_IST_Grombühl_alle.cpg
│ ├──  Baum_IST_Grombühl_alle.dbf
│ ├──  Baum_IST_Grombühl_alle.prj
│ ├──  Baum_IST_Grombühl_alle.sbn
│ ├──  Baum_IST_Grombühl_alle.sbx
│ ├──  Baum_IST_Grombühl_alle.shp
│ ├── 謹 Baum_IST_Grombühl_alle.shp.xml
│ ├──  Baum_IST_Grombühl_alle.shx
│ ├──  Grombuehl_Block_15-17.gml
│ ├──  trees_75_15.cpg
│ ├──  trees_75_15.dbf
│ ├──  trees_75_15.prj
│ ├──  trees_75_15.sbn
│ ├──  trees_75_15.sbx
│ ├──  trees_75_15.shp
│ ├── 謹 trees_75_15.shp.xml
│ └──  trees_75_15.shx
├──  250Bäume
│ ├──  Abstand5m
│ │ ├──  Grombuehl_Block_15-17.gml
│ │ ├──  trees_250_5.cpg
│ │ ├──  trees_250_5.dbf
│ │ ├──  trees_250_5.prj
│ │ ├──  trees_250_5.sbn
│ │ ├──  trees_250_5.sbx
│ │ ├──  trees_250_5.shp
│ │ ├── 謹 trees_250_5.shp.xml
│ │ └──  trees_250_5.shx
│ ├──  Abstand15m
│ │ ├──  49_801844__49_802502__9_948075__9_949534_15m_Block24
│ │ │ ├──  Grombuehl_Block_24.gml
│ │ │ ├──  trees_250_15_kl.cpg
│ │ │ ├──  trees_250_15_kl.dbf
│ │ │ ├──  trees_250_15_kl.prj
│ │ │ ├──  trees_250_15_kl.shp
│ │ │ └──  trees_250_15_kl.shx
│ │ └──  49_802605__49_804087__9_94036__9_948417_15m_Block7-11_14-17
│ │ ├──  Grombuehl_Block_7-11_14-17.gml
│ │ ├──  trees_150_15_gr.cpg
│ │ ├──  trees_150_15_gr.dbf
│ │ ├──  trees_150_15_gr.prj
│ │ ├──  trees_150_15_gr.sbn
│ │ ├──  trees_150_15_gr.sbx
│ │ ├──  trees_150_15_gr.shp
│ │ ├── 謹 trees_150_15_gr.shp.xml
│ │ └──  trees_150_15_gr.shx
│ ├──  Baum_IST_Grombühl_alle.cpg
│ ├──  Baum_IST_Grombühl_alle.dbf
│ ├──  Baum_IST_Grombühl_alle.prj
│ ├──  Baum_IST_Grombühl_alle.sbn
│ ├──  Baum_IST_Grombühl_alle.sbx
│ ├──  Baum_IST_Grombühl_alle.shp
│ ├── 謹 Baum_IST_Grombühl_alle.shp.xml
│ └──  Baum_IST_Grombühl_alle.shx
├──  500Bäume
│ ├──  Abstand10m
│ │ ├──  49_801844__49_802502__9_948075__9_949534_10m_block_24
│ │ │ ├──  Grombuehl_Block_24.gml
│ │ │ ├──  trees_500_10_kl.cpg
│ │ │ ├──  trees_500_10_kl.dbf
│ │ │ ├──  trees_500_10_kl.prj
│ │ │ ├──  trees_500_10_kl.shp
│ │ │ └──  trees_500_10_kl.shx
│ │ └──  49_802605__49_804087__9_94036__9_948417_10m_block_7-11_14-17
│ │ ├──  Grombuehl_Block_7-11_14-17.gml
│ │ ├──  trees_500_10_gr.cpg
│ │ ├──  trees_500_10_gr.dbf
│ │ ├──  trees_500_10_gr.prj
│ │ ├──  trees_500_10_gr.sbn
│ │ ├──  trees_500_10_gr.sbx
│ │ ├──  trees_500_10_gr.shp
│ │ ├── 謹 trees_500_10_gr.shp.xml
│ │ └──  trees_500_10_gr.shx
│ ├──  Baum_IST_Grombühl_alle.cpg
│ ├──  Baum_IST_Grombühl_alle.dbf
│ ├──  Baum_IST_Grombühl_alle.prj
│ ├──  Baum_IST_Grombühl_alle.sbn
│ ├──  Baum_IST_Grombühl_alle.sbx
│ ├──  Baum_IST_Grombühl_alle.shp
│ ├── 謹 Baum_IST_Grombühl_alle.shp.xml
│ └──  Baum_IST_Grombühl_alle.shx
├──  file_structure.txt
└──  ganzes_gebiet
├──  Grombühl_BA_IST.gml
├──  trees_75_15.cpg
├──  trees_75_15.dbf
├──  trees_75_15.prj
├──  trees_75_15.sbn
├──  trees_75_15.sbx
├──  trees_75_15.shp
├── 謹 trees_75_15.shp.xml
├──  trees_75_15.shx
├──  trees_250_5.cpg
├──  trees_250_5.dbf
├──  trees_250_5.prj
├──  trees_250_5.sbn
├──  trees_250_5.sbx
├──  trees_250_5.shp
├── 謹 trees_250_5.shp.xml
├──  trees_250_5.shx
├──  trees_250_15.cpg
├──  trees_250_15.dbf
├──  trees_250_15.prj
├──  trees_250_15.sbn
├──  trees_250_15.sbx
├──  trees_250_15.shp
├── 謹 trees_250_15.shp.xml
├──  trees_250_15.shx
├──  trees_500_10.cpg
├──  trees_500_10.dbf
├──  trees_500_10.prj
├──  trees_500_10.sbn
├──  trees_500_10.sbx
├──  trees_500_10.shp
├──  trees_500_10.shx
├── 謹 trees_500_10.xml
├──  trees_500_15.cpg
├──  trees_500_15.dbf
├──  trees_500_15.prj
├──  trees_500_15.sbn
├──  trees_500_15.sbx
├──  trees_500_15.shp
├── 謹 trees_500_15.shp.xml
└──  trees_500_15.shx
......@@ -7,7 +7,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFinder;
import org.geotools.data.FeatureSource;
......@@ -17,13 +16,15 @@ import org.locationtech.jts.geom.Point;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
public class TreeKatasterData {
public class TreeKatasterData
{
public static final double TRUNK_PERCENTAGE = 0.2;
public static final double CROWN_PERCENTAGE = 1 - TRUNK_PERCENTAGE;
private List<Tree> trees;
public static TreeKatasterData parseTreeKatasterData(Path path) throws IOException {
TreeKatasterData result = new TreeKatasterData();
Map<String, Object> readParameters = new HashMap<>();
......@@ -75,7 +76,7 @@ public class TreeKatasterData {
}
return result;
}
public List<Tree> getTrees() {
if (trees == null) {
trees = new ArrayList<>();
......
......@@ -17,7 +17,9 @@ import org.xmlobjects.gml.model.geometry.aggregates.MultiSurface;
import org.xmlobjects.gml.model.geometry.aggregates.MultiSurfaceProperty;
import org.xmlobjects.gml.model.measures.Length;
public class TreeUtils {
public class TreeUtils
{
private static final GeometryFactory FACTORY = new GeometryFactory();
......@@ -36,10 +38,10 @@ public class TreeUtils {
// generateTreesFromKataster(cityModel, katasterData2);
// All kataster trees are taken, osm trees are removed
// filterDuplicateTreesFromOSM(osmData, katasterData);
// filterDuplicateTreesFromOSM(osmData, katasterData);
// filterDuplicateTreesFromOSM(osmData, katasterData2);
// generateTreesFromOSM(cityModel, osmData, wktPolygon);
// generateTreesFromOSM(cityModel, osmData, wktPolygon);
}
......
package de.hft.stuttgart.scripts;
import java.io.File;
import java.io.IOException;
import org.citygml4j.xml.CityGMLContextException;
import org.citygml4j.xml.reader.CityGMLReadException;
import org.citygml4j.xml.writer.CityGMLWriteException;
import org.locationtech.jts.io.ParseException;
import de.hft.stuttgart.citygml.green.osm.GreenEnricher;
import jakarta.xml.bind.JAXBException;
public class PrepareDataForMarieke
{
// Add Trees to GML files inside data/Marieke/...
public static void main(String[] whatever) throws Exception {
processWholeRegion();
processSmallBlocks();
processStatusQuo();
}
private static void processStatusQuo() throws IOException, CityGMLContextException, CityGMLReadException,
InterruptedException, CityGMLWriteException, JAXBException, ParseException {
File folder = new File("data/Marieke/GebietsausschnittStatusQuo/");
File shapefile = new File("data/Marieke/GebietsausschnittStatusQuo/Baum_alle.shp");
File[] gmls = folder.listFiles((dir, name) -> name.endsWith(".gml"));
for (File gml : gmls) {
System.err.println(">>> " + gml);
String extension = shapefile.getName().replaceAll("\\.shp", "");
String[] args = new String[] { gml.toString(), // Input GML
shapefile.toString(), // Added trees, in Baumkatasterformat,
extension, // Output GML suffix
};
GreenEnricher.main(args);
}
}
private static void processSmallBlocks() throws IOException, CityGMLContextException, CityGMLReadException,
InterruptedException, CityGMLWriteException, JAXBException, ParseException {
File[] folders = {
new File("data/Marieke/75Bäume/"),
new File("data/Marieke/250Bäume/Abstand5m"),
new File("data/Marieke/250Bäume/Abstand15m/49_801844__49_802502__9_948075__9_949534_15m_Block24"),
new File("data/Marieke/250Bäume/Abstand15m/49_802605__49_804087__9_94036__9_948417_15m_Block7-11_14-17"),
new File("data/Marieke/500Bäume/Abstand10m/49_801844__49_802502__9_948075__9_949534_10m_block_24"),
new File("data/Marieke/500Bäume/Abstand10m/49_802605__49_804087__9_94036__9_948417_10m_block_7-11_14-17"),
};
for (File folder : folders) {
System.err.println("### " + folder);
String gml = folder.listFiles((dir, name) -> name.endsWith(".gml"))[0].getName();
File[] shapefiles = folder.listFiles((dir, name) -> name.endsWith(".shp"));
for (File shapefile : shapefiles) {
System.out.println(shapefile);
String extension = shapefile.getName().replaceAll("\\.shp", "");
String[] args = new String[] { folder.toPath().resolve(gml).toString(), // Input GML
shapefile.toString(), // Added trees, in Baumkatasterformat,
extension, // Output GML suffix
};
GreenEnricher.main(args);
}
}
}
private static void processWholeRegion() throws IOException, CityGMLContextException, CityGMLReadException,
InterruptedException, CityGMLWriteException, JAXBException, ParseException {
File folder = new File("data/Marieke/ganzes_gebiet/");
String gml = "Grombühl_BA_IST.gml";
File[] shapefiles = folder.listFiles((dir, name) -> name.endsWith(".shp"));
for (File shapefile : shapefiles) {
System.err.println(">>> " + shapefile);
String extension = shapefile.getName().replaceAll("\\.shp", "");
String[] args = new String[] { folder.toPath().resolve(gml).toString(), // Input GML
shapefile.toString(), // Added trees, in Baumkatasterformat,
extension, // Output GML suffix
};
GreenEnricher.main(args);
}
}
}
......@@ -40,4 +40,4 @@ class AlkisGreenEnricherTest
GreenEnricher.main(args);
assertTrue(Files.exists(outputGML));
}
}
}
\ No newline at end of file
......@@ -8,7 +8,8 @@ Trees are exported in a CSV table, a PNG diagram and an HTML interactive map.
"""
import pickle
from pathlib import Path
from collections import namedtuple
from collections import namedtuple, Counter
import re
import folium
import matplotlib.pyplot as plt
......@@ -30,7 +31,9 @@ from import_existing_trees import get_existing_forest
# TODO: Write tests?
# From RegionChooser, or https://transfer.hft-stuttgart.de/gitlab/circulargreensimcity/circulargreensimcity/-/wikis/Fallstudien/Gromb%C3%BChl
WKT = "POLYGON((9.947021 49.803063, 9.947011 49.800917, 9.955025 49.800810, 9.955110 49.803019, 9.947021 49.803063))"
# WKT = "POLYGON((9.947021 49.803063, 9.947011 49.800917, 9.955025 49.800810, 9.955110 49.803019, 9.947021 49.803063))"
# Grafenbühl
WKT = "POLYGON((9.147551 48.908059, 9.148635 48.907953, 9.149525 48.907819, 9.151177 48.907819, 9.151413 48.907840, 9.153226 48.908087, 9.153387 48.906705, 9.149160 48.906634, 9.148999 48.906620, 9.147551 48.908059))"
# Replace with None if no existing tree should be imported
EXISTING_TREES = 'existing_trees/Trees_ideal_2_20240227.shp'
# EXISTING_TREES = 'existing_trees/baumkataster/Baum.shp'
......@@ -41,9 +44,15 @@ TREE_DISTANCE = 10 # [m]
MIN_DISTANCE = TREE_DISTANCE * 0.5 # [m]
# For display purposes only:
GRID = 100 # [m]
IGNORE_ROADS = set(['primary', 'unclassified', 'secondary',
'secondary_link', 'trunk', 'trunk_link', 'primary_link'])
# If streets from OSM don't have known width, which one should be used? In [m]
# Leave empty to get default from similar streets, if available
# Set to -1 if you want to disable trees along the roads
DEFAULT_WIDTHS = {
'unclassified': 0,
# 'residential': 8,
# 'motorway': -1,
# 'trunk': -1,
}
SCRIPT_DIR = Path(__file__).resolve().parent
......@@ -51,17 +60,17 @@ OUTPUT_DIR = SCRIPT_DIR / 'output'
Bounds = namedtuple("Bounds", "W S E N")
def load_region(wkt_polygon):
def load_region(wkt_polygon: str):
region = wkt.loads(wkt_polygon)
bounds = Bounds(*region.bounds)
return region, bounds
def get_basename(bounds):
def get_basename(bounds: Bounds):
return f'{bounds.S}__{bounds.N}__{bounds.W}__{bounds.E}_{TREE_DISTANCE}m'.replace('.', '_')
def get_osm_roads(bounds):
def get_osm_roads(bounds: Bounds):
cache_dir = SCRIPT_DIR / 'cache'
cache_dir.mkdir(exist_ok=True)
......@@ -88,7 +97,7 @@ def get_osm_roads(bounds):
return ways
def set_plot(bounds, to_local_coordinates):
def set_plot(bounds: Bounds, to_local_coordinates):
x_min, y_min = to_local_coordinates.transform(bounds.W, bounds.S)
x_max, y_max = to_local_coordinates.transform(bounds.E, bounds.N)
ax = plt.axes()
......@@ -103,16 +112,51 @@ def set_plot(bounds, to_local_coordinates):
return ax
def place_trees(forest, ways, region, to_local, tree_distance, min_distance_2) -> Forest:
def get_width(way: overpy.Way) -> float:
width_str = way.tags.get("width", '0')
# NOTE: Some widths are written with units, so try to remove trailing [m] first.
width_str = re.sub(' ?m$', '', width_str)
return float(width_str)
def get_default_widths(ways: list) -> dict[str, float]:
"""Check existing OSM highways, and extract the most common width for each type"""
width_counters: dict[str, Counter] = {}
for way in ways:
width = get_width(way)
way_type = way.tags.get("highway")
if width:
if way_type not in width_counters:
width_counters[way_type] = Counter()
width_counters[way_type][width] += 1
return {w: c.most_common(1)[0][0] for w, c in width_counters.items()}
def place_trees(forest: Forest, ways: list, region: str, to_local, tree_distance: float, min_distance_2: float) -> Forest:
local_region = transform(to_local.transform, region)
default_widths = {**get_default_widths(ways), **DEFAULT_WIDTHS}
print(f"Default widths: {default_widths}")
for way in ways:
width = float(way.tags.get("width", 0))
highway = way.tags.get("highway")
if highway in IGNORE_ROADS:
color = 'orange'
way_type = way.tags.get("highway")
width = get_width(way)
default_width = default_widths.get(way_type, 0)
if default_width < 0:
# Ignore this type of streets
continue
if width:
# Defined in OSM
color = 'blue'
else:
color = 'gray'
if default_width:
# From OSM most common width
color = 'orange'
width = default_width
else:
# Unknown
color = 'gray'
road_xy_s = [to_local.transform(node.lon, node.lat) for node in way.nodes]
......@@ -123,15 +167,13 @@ def place_trees(forest, ways, region, to_local, tree_distance, min_distance_2) -
tree_path = road_as_polygon.exterior
displayed_width = width
else:
# NOTE: Could try to guess width depending on highway type.
displayed_width = 1
road_xs, road_ys = zip(*road_xy_s)
plt.plot(road_xs, road_ys, linewidth=displayed_width, c=color, alpha=0.8, zorder=-1)
distances = np.arange(0, tree_path.length, tree_distance)
potential_trees = [tree_path.interpolate(
distance) for distance in distances]
potential_trees = [tree_path.interpolate(distance) for distance in distances]
if tree_path.boundary:
potential_trees += [tree_path.boundary.geoms[-1]]
......@@ -145,6 +187,7 @@ def place_trees(forest, ways, region, to_local, tree_distance, min_distance_2) -
description='Tilia tomentosa',
diameter=6,
height=10,
trunk_diameter=0.5,
source='add_trees.py'
)
......@@ -164,7 +207,7 @@ def plot_trees(bounds: Bounds, forest: Forest, tree_distance: float) -> None:
print(" DONE!")
def export_map(bounds, forest, epsg_id) -> None:
def export_map(bounds: Bounds, forest: Forest, epsg_id: int) -> None:
print("Exporting Map...")
to_wgs84 = Transformer.from_crs(f"EPSG:{epsg_id}", "EPSG:4326", always_xy=True)
interactive_map = folium.Map()
......@@ -197,7 +240,7 @@ def export_map(bounds, forest, epsg_id) -> None:
print(" DONE!")
def export_csv(bounds, forest, wkt_polygon, tree_distance, min_distance, epsg_id) -> None:
def export_csv(bounds: Bounds, forest: Forest, wkt_polygon: str, tree_distance: float, min_distance: float, epsg_id: int) -> None:
print("Exporting CSV...")
with open(OUTPUT_DIR / f"{get_basename(bounds)}_trees.csv", "w") as csv:
csv.write(f"# Fake trees for; {wkt_polygon}\n")
......@@ -212,7 +255,7 @@ def export_csv(bounds, forest, wkt_polygon, tree_distance, min_distance, epsg_id
print(" DONE!")
def export_shapefile(bounds: Bounds, forest: Forest, epsg_id: str) -> None:
def export_shapefile(bounds: Bounds, forest: Forest, epsg_id: int) -> None:
print("Exporting shapefile")
data = [{
......@@ -239,7 +282,7 @@ def export_shapefile(bounds: Bounds, forest: Forest, epsg_id: str) -> None:
print(" DONE!")
def main(wkt_polygon, epsg_id, tree_distance, min_distance, import_tree_shp) -> None:
def main(wkt_polygon: str, epsg_id: int, tree_distance: float, min_distance: float, import_tree_shp) -> None:
region, bounds = load_region(wkt_polygon)
ways = get_osm_roads(bounds)
......
......@@ -5,18 +5,20 @@ from tree import Tree, Forest
def get_existing_forest(shp_input):
print(f"Importing {shp_input}")
df = gpd.read_file(shp_input)
trees = []
forest = Forest()
for tree_row in df.itertuples():
point = tree_row.geometry
trees.append(Tree(point.x, point.y,
added = forest.add_tree_if_possible(0.1, point.x, point.y,
description=tree_row.Bezeichnun,
diameter=tree_row.Kronenbrei,
type=tree_row.Baumart,
trunk_diameter=tree_row.Stammumfan,
height=tree_row.Baumhöhe,
source=Path(shp_input).name
))
return Forest(trees)
)
if not added:
print(f"WARNING! tree seems to be too close to others! Is it a duplicate?\n\t{tree_row}")
return forest
if __name__ == "__main__":
print(repr(get_existing_forest('existing_trees/Trees_ideal_2_20240227.shp')))
......
......@@ -10,8 +10,8 @@ class Tree:
diameter: float
height: float
z: float = 0
trunk_diameter: float = None
type: str = None
trunk_diameter: float|None = None
type: str|None = None
description: str = '?'
source: str = '?'
color: str = 'green'
......@@ -39,11 +39,13 @@ class Forest(UserList):
self.data = existing_trees
def add_tree_if_possible(self, min_distance_2, x, y, **kparams):
def add_tree_if_possible(self, min_distance_2, x, y, **kparams) -> bool:
_nearest_tree, distance_2 = self.kd_tree.search_nn((x, y))
if distance_2 > min_distance_2:
self.kd_tree.add((x, y))
self.append(Tree(x, y, **kparams))
return True
return False
@property
def xs_ys_cs(self):
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment