Skip to content
This repository was archived by the owner on Jul 11, 2025. It is now read-only.

Commit 807455f

Browse files
Merge pull request #6 from open-AIMS/parq-time
Add assessment methods for parquet file assessment
2 parents 1d4b296 + b17cb1d commit 807455f

File tree

6 files changed

+415
-198
lines changed

6 files changed

+415
-198
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ The following processing is required before use:
129129
- Simplified reef polygons should be provided as vertex-vertex lines with `polygon_to_lines()`.
130130
- Require raster of target pixels to search, and their indices (currently a vector of `CartesianIndices` for identifying search pixels). Use `findall(bool_search_raster)` to return pixel indices.
131131
- Raster of search pixels should be masked by reef polygons or simplified reef polygons.
132+
- The target region name should be specified in GBRMPA format.
133+
- E.g. "Townsville/Whitsunday Management Area" rather than "Townsville-Whitsunday".
134+
135+
### Parquet assessment additional setup
136+
137+
- A parquet GeoDataFrame must be loaded and filtered for unsuitable pixels based on user criteria thresholds using a Dict and `within_thresholds()`.
138+
- `lon` and `lat` columns (FLoat64) must be added to the GeoDataFrame.
139+
- E.g. `valid_pixels.lon = first.(GI.coordinates.(valid_pixels.geometry))`
132140
The column used for masking should be the same as the column specified as geometry_col in
133141
`identify_potential_sites_edges` (default = `:geometry`).
134142

src/ReefGuideAPI.jl

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ using
2323
Oxygen
2424

2525
include("assessment/criteria.jl")
26-
include("geom_handlers/site_assessment.jl")
26+
include("geom_handlers/common_assessment.jl")
27+
include("geom_handlers/raster_site_assessment.jl")
28+
include("geom_handlers/parq_site_assessment.jl")
2729
include("assessment/query_thresholds.jl")
2830
include("Middleware.jl")
2931

@@ -248,7 +250,9 @@ export
248250
export
249251
create_poly,
250252
create_bbox,
251-
port_buffer_mask
253+
port_buffer_mask,
254+
meters_to_degrees,
255+
polygon_to_lines
252256

253257
# Raster->Index interactions (defunct?)
254258
export
@@ -257,4 +261,8 @@ export
257261
valid_flat_lon_inds,
258262
valid_flat_lat_inds
259263

264+
# Ruleset thresholds
265+
export
266+
within_thresholds
267+
260268
end
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
""" Functions common to both site_assessment methods. """
2+
3+
using LinearAlgebra
4+
5+
include("geom_ops.jl")
6+
7+
# Additional functions for reef-edge alignment processing.
8+
"""
9+
meters_to_degrees(x, lat)
10+
11+
Convert meters to degrees at target latitude.
12+
"""
13+
function meters_to_degrees(x, lat)
14+
return x / (111.1 * 1000 * cosd(lat))
15+
end
16+
17+
"""
18+
degrees_to_meters(x, lat)
19+
20+
Convert degrees to meters at target latitude.
21+
"""
22+
function degrees_to_meters(x, lat)
23+
return x * (111.1 * 1000 * cosd(lat))
24+
end
25+
26+
"""
27+
from_zero(v::Vector{Tuple{Float64,Float64}})::Vector{Tuple{Float64, Float64}}
28+
29+
Translates Vector of points `v` to begin from (0, 0), retaining direction and length.
30+
31+
# Argument
32+
- `v` : Vector of point coordinates (`Tuple{Float64, Float64}`).
33+
"""
34+
function from_zero(v::Vector{Tuple{Float64,Float64}})::Vector{Tuple{Float64,Float64}}
35+
max_coord = maximum(v)
36+
if first(v) == max_coord
37+
v = reverse(v)
38+
end
39+
40+
new_coords = [
41+
Vector{Union{Missing,Float64}}(missing, size(max_coord, 1)),
42+
Vector{Union{Missing,Float64}}(missing, size(max_coord, 1))
43+
]
44+
for (j, coords) in enumerate(new_coords)
45+
for val in eachindex(coords)
46+
coords[val] = max_coord[val] - v[j][val]
47+
end
48+
end
49+
50+
return Tuple.(new_coords)
51+
end
52+
53+
"""
54+
line_angle(a::T, b::T)::Float64 where {T <: Vector{Tuple{Float64,Float64}}}
55+
56+
Calculate the angle between two lines.
57+
58+
# Arguments
59+
- `a` : Line between point coordinates.
60+
- `b` : Line between point coordinates.
61+
62+
# Returns
63+
Angle between the two lines.
64+
65+
# Examples
66+
```julia
67+
line_angle([(0.0,5.0), (0.0,0.0)], from_zero(edge_line))
68+
line_angle([(0.0,5.0), (0.0,0.0)], [(1.0, 4.0), (7.0, 8.0)])
69+
```
70+
"""
71+
function line_angle(a::T, b::T)::Float64 where {T<:Vector{Tuple{Float64,Float64}}}
72+
return acosd(clamp(a b / (norm(a) * norm(b)), -1, 1))
73+
end
74+
75+
"""
76+
filter_far_polygons(gdf::DataFrame, pixel::GIWrap.Point, lat::Float64)::BitVector
77+
78+
Filter out reefs that are > 10km from the target pixel (currently hardcoded threshold).
79+
"""
80+
function filter_far_polygons(gdf::DataFrame, pixel::GeometryBasics.Point, lat::Float64)::BitVector
81+
return (GO.distance.(GO.centroid.(gdf[:, first(GI.geometrycolumns(gdf))]), [pixel]) .< meters_to_degrees(10000, lat))
82+
end
83+
84+
"""
85+
initial_search_box(
86+
(lon::Float64, lat::Float64),
87+
x_dist::Union{Int64, Float64},
88+
y_dist::Union{Int64, Float64},
89+
target_crs::GeoFormatTypes.CoordinateReferenceSystemFormat,
90+
res::Float64
91+
)::GI.Wrappers.Polygon
92+
93+
Create an initial search box that is centered around the point `(lon, lat)` in `target_crs`,
94+
and is buffered by `res` distance.
95+
96+
# Arguments
97+
- `(lon, lat)` : Longitude and latitude coordinates of the center target pixel.
98+
- `x_dist` : x (longitude) dimension length of initial search box.
99+
- `y_dist` : y (latitude) dimension length of initial search box.
100+
- `target_crs` : Target CRS of box to match input data types.
101+
- `res` : Buffer distance (resolution of input raster search data).
102+
103+
# Returns
104+
- Initial search box geometry.
105+
"""
106+
function initial_search_box(
107+
(lon, lat),
108+
x_dist::Union{Int64,Float64},
109+
y_dist::Union{Int64,Float64},
110+
target_crs::GeoFormatTypes.CoordinateReferenceSystemFormat,
111+
res::Float64
112+
)::GI.Wrappers.Polygon
113+
lon_dist = meters_to_degrees(x_dist, lat)
114+
xs = (lon - lon_dist / 2, lon + lon_dist / 2)
115+
lat_dist = meters_to_degrees(y_dist, lat)
116+
ys = (lat - lat_dist / 2, lat + lat_dist / 2)
117+
118+
search_plot = create_poly(create_bbox(xs, ys), target_crs)
119+
geom_buff = GO.buffer(search_plot, res)
120+
121+
return geom_buff
122+
end
123+
124+
"""
125+
identify_closest_edge(
126+
pixel::GeometryBasics.Point{2, Float64},
127+
reef_lines::Vector{GeometryBasics.Line{2, Float64}}
128+
)::Vector{Tuple{Float64, Float64}}
129+
130+
Find the nearest line in `reef_lines` to a point `pixel`.
131+
132+
# Arguments
133+
- `pixel` : Target point geometry.
134+
- `reef_lines` : Vector containing lines for comparison.
135+
"""
136+
function identify_closest_edge(
137+
pixel::GeometryBasics.Point{2,Float64},
138+
reef_lines::Vector{GeometryBasics.Line{2,Float64}}
139+
)::Vector{Tuple{Float64,Float64}}
140+
nearest_edge = reef_lines[argmin(GO.distance.([pixel], reef_lines))]
141+
142+
return [tuple(x...) for x in nearest_edge]
143+
end
144+
145+
"""
146+
initial_search_rotation(
147+
pixel::GeometryBasics.Point{2, Float64},
148+
geom_buff::GI.Wrappers.Polygon,
149+
gdf::DataFrame,
150+
reef_outlines::Vector{Vector{GeometryBasics.Line{2, Float64}}}
151+
)::Float64
152+
153+
Identifies the closest edge to the target `pixel`/'`geom_buff` and returns the initial rotation
154+
angle required to match the edge line.
155+
156+
# Arguments
157+
- `pixel` : Target point at the center of the search polygon.
158+
- `geom_buff` : Initial search box with zero rotation.
159+
- `gdf` : GeoDataFrame containing a geometry column used for pixel masking.
160+
- `reef_outlines` : Line segments for the outlines of each reef in `gdf`.
161+
"""
162+
function initial_search_rotation(
163+
pixel::GeometryBasics.Point{2,Float64},
164+
geom_buff::GI.Wrappers.Polygon,
165+
gdf::DataFrame,
166+
reef_outlines::Vector{Vector{GeometryBasics.Line{2,Float64}}}
167+
)::Float64
168+
distance_indices = filter_far_polygons(gdf, pixel, pixel[2])
169+
reef_lines = reef_outlines[distance_indices]
170+
reef_lines = reef_lines[
171+
GO.within.([pixel], gdf[distance_indices, first(GI.geometrycolumns(gdf))])
172+
]
173+
reef_lines = vcat(reef_lines...)
174+
175+
# If a pixel is outside of a polygon, use the closest polygon instead.
176+
if isempty(reef_lines)
177+
reef_distances = GO.distance.(
178+
[pixel],
179+
gdf[distance_indices, first(GI.geometrycolumns(gdf))]
180+
)
181+
reef_lines = reef_outlines[distance_indices]
182+
reef_lines = reef_lines[argmin(reef_distances)]
183+
reef_lines = vcat(reef_lines...)
184+
end
185+
186+
edge_line = identify_closest_edge(pixel, reef_lines)
187+
188+
# Calculate the angle between the two lines
189+
edge_bearing = line_angle([(0.0, 5.0), (0.0, 0.0)], from_zero(edge_line))
190+
rot_angle = line_angle(from_zero(find_horizontal(geom_buff)), from_zero(edge_line))
191+
if edge_bearing > 90
192+
rot_angle = -rot_angle
193+
end
194+
195+
return rot_angle
196+
end
197+
198+
"""
199+
filter_intersecting_sites(res_df::DataFrame)::DataFrame
200+
201+
Filter out sites where the qc_flag indicates a suitabiltiy < `surr_threshold` in searching.
202+
Identify and keep the highest scoring site polygon where site polygons are overlapping.
203+
204+
# Arguments
205+
- `res_df` : Results DataFrame containing potential site polygons (output from
206+
`identify_potential_sites()` or `identify_potential_sites_edges()`).
207+
"""
208+
function filter_intersecting_sites(res_df::DataFrame)::DataFrame
209+
res_df.row_ID = 1:size(res_df, 1)
210+
ignore_list = []
211+
212+
for (row) in eachrow(res_df)
213+
if row.row_ID ignore_list
214+
continue
215+
end
216+
217+
if row.qc_flag == 1
218+
push!(ignore_list, row.row_ID)
219+
continue
220+
end
221+
222+
if any(GO.intersects.([row.poly], res_df[:, :poly]))
223+
intersecting_polys = res_df[(GO.intersects.([row.poly], res_df[:, :poly])), :]
224+
if maximum(intersecting_polys.score) <= row.score
225+
for x_row in eachrow(intersecting_polys[intersecting_polys.row_ID .!= row.row_ID, :])
226+
push!(ignore_list, x_row.row_ID)
227+
end
228+
else
229+
push!(ignore_list, row.row_ID)
230+
end
231+
end
232+
end
233+
234+
return res_df[res_df.row_ID .∉ [unique(ignore_list)], :]
235+
end

src/geom_handlers/geom_ops.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ Find a horizontal line if one exists within a geometry.
157157
# Returns
158158
Vector containing tuples of coordinates for a horizontal line found within geom.
159159
"""
160-
function find_horizontal(geom::GI.Wrappers.Polygon)::Vector{Tuple{Float64,Float64}, Tuple{Float64,Float64}}
160+
function find_horizontal(geom::GIWrap.Polygon)::Vector{Tuple{Float64,Float64}}
161161
coords = collect(GI.coordinates(geom)...)
162162
first_coord = first(coords)
163163
second_coord = coords[

0 commit comments

Comments
 (0)