from __future__ import annotations
import logging
from typing import Any, List, Tuple
import geopandas as gpd
import matplotlib.patches as patches
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.lines import Line2D
from numpy.typing import NDArray
from scipy import interpolate
from shapely.geometry import LineString, Point, Polygon
from pyvibracore.results.plot_utils import _north_arrow, _scalebar
def _sound_prediction(
power: float, k2: float, period: float, levels: List[float]
) -> NDArray:
"""
Based on the 'Handleiding meten en rekenen industrielawaai' 2004 methode l.
More information: https://open.overheid.nl/Details/ronl-15eb5528-d835-4f6a-b3a1-fc0851b334f9/1
Parameters
----------
power:
source power [dB]
k2:
Correction term [dB]
period:
Operating period of the building code [hours]
levels: list
Array of floats to calculate the distance
Returns
-------
distance: NDArray
"""
distance = np.arange(1e-5, 500, step=0.2)
noise = (
power
- (-10 * np.log10(period / 12))
- (20 * np.log10(distance) + 0.005 * distance + 9.1)
+ k2
)
# interpolate and predict
f = interpolate.interp1d(
noise, distance, kind="cubic", assume_sorted=False, fill_value="extrapolate"
)
space = f(levels)
# raise warning
if any([item > 500 for item in space]):
logging.warning(
"One or more distances exceeds the 500 meter mark. "
"Please note that this methode extrapolate the values from this point."
)
return space
[docs]def map_sound(
buildings: gpd.GeoDataFrame,
source_location: Point | LineString | Polygon,
building_name: str,
power: float,
k2: float,
period: float,
title: str = "Legend:",
figsize: Tuple[float, float] = (10.0, 12.0),
settings: dict | None = None,
**kwargs: Any,
) -> plt.Figure:
"""
Create map of the input building settings.
Parameters
----------
buildings:
GeoDataFrame of the input buildings
source_location:
location of the vibration source
building_name:
name of the building
power:
source power [dB]
k2:
Correction term [dB]
period:
Operating period of the building code [hours]
title:
Legend title
figsize:
Size of the activate figure, as the `plt.figure()` argument.
settings:
Plot settings used in plot: default settings are:
.. code-block:: python
{
"source_location": {"label": "Trillingsbron", "color": "blue"},
"levels": [
{
"label": ">80 db [0 dagen]",
"level": 80,
"color": "darkred",
},
{
"label": ">75 db [5 dagen]",
"level": 75,
"color": "red",
},
{
"label": ">70 db [15 dagen]",
"level": 70,
"color": "orange",
},
{
"label": ">65 db [30 dagen]",
"level": 65,
"color": "darkgreen",
},
{
"label": ">60 db [50 dagen]",
"level": 60,
"color": "green",
},
],
}
**kwargs:
All additional keyword arguments are passed to the `pyplot.subplots()` call.
Returns
-------
Figure
"""
if settings is None:
settings = {
"source_location": {"label": "Trillingsbron", "color": "blue"},
"levels": [
{
"label": ">80 db [0 dagen]",
"level": 80,
"color": "darkred",
},
{
"label": ">75 db [5 dagen]",
"level": 75,
"color": "red",
},
{
"label": ">70 db [15 dagen]",
"level": 70,
"color": "orange",
},
{
"label": ">65 db [30 dagen]",
"level": 65,
"color": "darkgreen",
},
{
"label": ">60 db [50 dagen]",
"level": 60,
"color": "lightgreen",
},
],
}
kwargs_subplot = {
"figsize": figsize,
"tight_layout": True,
}
kwargs_subplot.update(kwargs)
fig, axes = plt.subplots(**kwargs_subplot)
gpd.GeoSeries(source_location).plot(
ax=axes, color=settings["source_location"]["color"], alpha=1, zorder=1, aspect=1
)
building = buildings.get(buildings["name"] == building_name)
if building.empty:
raise ValueError(f"No buildings with name {building_name}.")
building.where(buildings["name"] == building_name).plot(
ax=axes, zorder=2, color="gray", aspect=1
)
buildings.where(buildings["name"] != building_name).plot(
ax=axes, zorder=2, color="lightgray", aspect=1
)
# plot contour
levels = [values["level"] for values in settings["levels"]]
distances = _sound_prediction(power, k2, period, levels=levels)
colors = [values["color"] for values in settings["levels"]]
for distance, color in zip(distances, colors):
gpd.GeoSeries(building.buffer(distance).exterior).plot(
ax=axes, zorder=3, color=color, aspect=1
)
# plot name
for idx, row in buildings.iterrows():
x = row.geometry.centroid.xy[0][0]
y = row.geometry.centroid.xy[1][0]
axes.annotate(
idx,
xy=(x, y),
horizontalalignment="center",
)
# add legend
axes.legend(
title=title,
title_fontsize=18,
fontsize=15,
loc="lower right",
handles=[
patches.Patch(
facecolor=settings["source_location"]["color"],
label=settings["source_location"]["label"],
alpha=0.9,
linewidth=2,
edgecolor="black",
)
]
+ [
Line2D(
[0],
[0],
color=value["color"],
label=value["label"],
alpha=0.9,
linewidth=2,
)
for value in settings["levels"]
],
)
_north_arrow(axes)
_scalebar(axes)
return fig
[docs]def get_normative_building(
buildings: gpd.GeoDataFrame,
location: Polygon | LineString | Point,
) -> str | None:
"""
Get the name of the closest building with one of the follwing category:
- "woonfunctie",
- "gezondheidsfunctie",
- "onderwijsfunctie"
Parameters
----------
buildings:
GeoDataFrame that holds the building information
location:
Geometry of the source location
Returns
-------
name: str
"""
category = ["woonfunctie", "gezondheidsfunctie", "onderwijsfunctie"]
gdf = buildings.get(
[
any(item in category for item in row.split(",")) if row else False
for row in buildings["gebruiksdoel"].to_list()
]
)
if gdf.empty:
logging.error(f"ValueError: No buildings with category {category}.")
return None
# FIXME: SettingWithCopyWarning
with pd.option_context("mode.chained_assignment", None):
gdf["distance"] = gdf.distance(location)
return gdf.sort_values("distance", na_position="last").iloc[0].get("name")