Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds test data and description #27

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ node_modules
web/public/
data_prep/inspire
data_prep/local_inputs
test_cases/test_case_*_inspire_parcels.geojson
test_cases/test_case_*_inspire_parcels_27700.geojson
9 changes: 5 additions & 4 deletions cli/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ flatgeobuf = { git = "https://github.com/michaelkirk/flatgeobuf/", branch = "mki
geo = "0.28.0"
geojson = { git = "https://github.com/georust/geojson", features = ["geo-types"] }
geozero = { version = "0.13.0", default-features = false, features = ["with-geo"] }
serde = "1.0.210"
serde_json = "1.0.117"
utils = { git = "https://github.com/a-b-street/utils" }
widths = { path = "../widths" }
21 changes: 21 additions & 0 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Classifies Inspire polygons into "road space" and "non-road space" polygons.
// Where necessary it will subdivide polygons into smaller polygons.
// It will also attempt to classify the negative space between polygons.


use std::collections::HashSet;

use serde::{Deserialize, Serialize};


#[derive(Serialize, Deserialize)]
pub struct SourceInspirePolygons {
geometry: geojson::Geometry,
}

pub fn classify_polygons_by_id(
_polygons: Vec<SourceInspirePolygons>
) -> (Option<HashSet<usize>>, Option<HashSet<usize>>) {
// TODO
unimplemented!();
}
134 changes: 134 additions & 0 deletions cli/tests/test_inspire_classifier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use std::collections::HashSet;

mod utils;

use cli::{classify_polygons_by_id, SourceInspirePolygons};

use std::fs::File;
use std::io::BufReader;



struct InspireClassificationExpectedResults {
case_id: usize,
road_space_polygons: Option<HashSet<usize>>,
non_road_space_polygons: Option<HashSet<usize>>,
}

fn get_expected_results() -> Vec<InspireClassificationExpectedResults> {
// See `test_cases.md`` for details
vec![
InspireClassificationExpectedResults {
case_id: 1,
road_space_polygons: Some(HashSet::from([
25855822,
25852274,
25853082,
25857333,
25853200,
25853129,
25853929,
])),
non_road_space_polygons: None,
},
InspireClassificationExpectedResults {
case_id: 3,
road_space_polygons: Some(HashSet::from([
25809009,
])),
non_road_space_polygons: Some(HashSet::from([
25803085,
25803248,
54395749,
])),
},
InspireClassificationExpectedResults {
case_id: 5,
road_space_polygons: Some(HashSet::from([
25826568,
])),
non_road_space_polygons: None
},
]
}


fn read_test_case_source_polygons(case_id: usize) -> Result<Vec<SourceInspirePolygons>, geojson::Error> {
// let f_name = format!("test_case_{}", case_id);
let f_path = format!("test_cases/test_case_{}_inspire_parcels.geojson", case_id);
let f_path = utils::get_test_file_path(f_path).unwrap();
dbg!("Reading test case source polygons from: {}", f_path.clone());

let file_reader = BufReader::new(File::open(f_path)?);

geojson::de::deserialize_feature_collection_to_vec::<SourceInspirePolygons>(file_reader)
}

fn check_expected_results(
expected_matches: &Option<HashSet<usize>>,
expected_non_matches: &Option<HashSet<usize>>,
actual_matches: &Option<HashSet<usize>>,
) {

// Handle cases where either the expected or actual results are `None`
match (expected_matches, actual_matches) {
(None, _) => {
// Both are `None`, so we're good
},
(Some(expected), Some(actual)) => {
// We want to check that we've identified all the expected road space
// polygons, but our 'expected' list might not be exhaustive.
assert!(
actual.is_superset(&expected)
)
},
(Some(_), None) => {
assert!(false, "Expected and actual road space polygons do not match");
}

}

// Check that we have mis-classified any road space polygons
match (expected_non_matches, actual_matches) {
(Some(expected), Some(actual)) => {
assert!(
actual.is_disjoint(&expected)
)
},
_ => {
// We're good
}
}

}


#[test]
fn test_classify_group_a() {

let expected_results = get_expected_results();

for case in expected_results.iter() {
let case_id = case.case_id;
let expected_road_space_ids = &case.road_space_polygons;
let expected_non_road_space_ids = &case.non_road_space_polygons;

let source_polygons = read_test_case_source_polygons(case_id).unwrap();
let (actual_road_space, actual_non_road_space) = classify_polygons_by_id(source_polygons);

// Check the road space polygons
check_expected_results(
&expected_road_space_ids,
&expected_non_road_space_ids,
&actual_road_space
);

// Check the non-road space polygons
check_expected_results(
&expected_non_road_space_ids,
&expected_road_space_ids,
&actual_non_road_space
);
}

}
45 changes: 45 additions & 0 deletions cli/tests/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use std::path::PathBuf;

use anyhow::{anyhow, Result};

/// Obtains a path to a test file (test code only!)
/// This is a convenience function for writing test code. It allow tests code from anywhere in the
/// workspace to access test files (eg input .osm files, golden outputfiles etc) which are stored within
/// the `tests` package.
/// This function make direct reference to the location of this source file (using the `file!()` marco)
/// and hence should only be used in test code and not in any production code.
// Copied from here:
// https://github.com/a-b-street/abstreet/blob/d30c36a22a87824d3581e0d0d4e2faf9788d6176/tests/src/lib.rs#L29
pub fn get_test_file_path(path: String) -> Result<String> {
// Get the absolute path to the crate that called was invoked at the cli (or equivalent)
let maybe_workspace_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let maybe_workspace_dir = std::path::Path::new(&maybe_workspace_dir);
// Get the relative path to this source file within the workspace
let this_source_file = String::from(file!());

// Try a find a suitable way to join the two paths to find something that exists
let test_file = next_test_file_path(maybe_workspace_dir, &this_source_file);
if test_file.is_ok() {
// Now try and match the workspace path with the user requested path
match next_test_file_path(test_file.as_ref().unwrap(), &path) {
Ok(pb) => Ok(String::from(pb.to_str().unwrap())),
Err(e) => Err(e),
}
} else {
panic!("Cannot find the absolute path to {}. Check that this function being called from test code, not production code.", this_source_file);
}
}

fn next_test_file_path(
maybe_absolute_dir: &std::path::Path,
file_path: &String,
) -> Result<PathBuf> {
let path_to_test = maybe_absolute_dir.join(file_path);
if path_to_test.exists() {
Ok(path_to_test)
} else if maybe_absolute_dir.parent().is_some() {
next_test_file_path(maybe_absolute_dir.parent().unwrap(), file_path)
} else {
Err(anyhow!("Cannot locate file '{}'", file_path))
}
}
38 changes: 38 additions & 0 deletions test_cases/get_test_data.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/bash

# This is just a convenience to download a subset of the data for test purposes.

set -e
set -x

this_dir=$(dirname $0)
AREA=Newcastle_City_Council
mkdir -p $AREA
pushd $AREA

wget https://use-land-property-data.service.gov.uk/datasets/inspire/download/$AREA.zip
unzip $AREA.zip Land_Registry_Cadastral_Parcels.gml

# Convert GML to GeoJSON, dropping all properties and fixing the coordinate system.
# See
# https://gis.stackexchange.com/questions/442709/how-can-i-debug-hanging-ogr2ogr
# for the amusing story of ignoring an unreachable schema in the GML.
ogr2ogr v1.geojson -oo DOWNLOAD_SCHEMA=NO Land_Registry_Cadastral_Parcels.gml -sql 'SELECT geometry FROM PREDEFINED'

popd

# Get the bounding boxes for all test cases
test_case_bboxes=`find $this_dir -type f -name "test_case_*_bounding_box.geojson"`

# Create a parcel file for each test case
for test_case_bbox in $test_case_bboxes; do
intermediate_file="${test_case_bbox/bounding_box/inspire_parcels_27700}"
output_file="${test_case_bbox/bounding_box/inspire_parcels}"

mapshaper $AREA/v1.geojson -clip $test_case_bbox -o $intermediate_file format=geojson geojson-type=FeatureCollection
# Convert to WGS84
ogr2ogr $output_file -s_srs EPSG:27700 -t_srs EPSG:4326 $intermediate_file
done

# Clean up intermediate files
rm -rf $AREA
8 changes: 8 additions & 0 deletions test_cases/test_case_1_bounding_box.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "FeatureCollection",
"name": "test_case_1_bounding_box",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::27700" } },
"features": [
{ "type": "Feature", "properties": { }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 424240, 568880 ], [ 424340, 568880 ], [ 424340, 570021 ], [ 424240, 570021 ], [ 424240, 568880 ] ] ] } }
]
}
8 changes: 8 additions & 0 deletions test_cases/test_case_2_bounding_box.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "FeatureCollection",
"name": "test_case_2_bounding_box",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::27700" } },
"features": [
{ "type": "Feature", "properties": { "test_case_name": "test_case_2", "width": 100.701894, "height": 186.276894, "area": 18758.436017, "perimeter": 573.957576 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 424336.028977272857446, 569400.797348484862596 ], [ 424436.730871212144848, 569400.797348484862596 ], [ 424436.730871212144848, 569587.074242424103431 ], [ 424336.028977272857446, 569587.074242424103431 ], [ 424336.028977272857446, 569400.797348484862596 ] ] ] } }
]
}
10 changes: 10 additions & 0 deletions test_cases/test_case_2_expected_non_road_space.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"type": "FeatureCollection",
"name": "test_case_2_missing_ploygons",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::27700" } },
"features": [
{ "type": "Feature", "properties": { "test_case_name": "case_2_missingploygon_1" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 424379.729999999981374, 569436.569999999948777 ], [ 424376.049999999988358, 569426.300000000046566 ], [ 424415.89000000001397, 569425.699999999953434 ], [ 424416.039999999979045, 569434.199999999953434 ], [ 424416.07, 569435.75 ], [ 424379.729999999981374, 569436.569999999948777 ] ] ] } },
{ "type": "Feature", "properties": { "test_case_name": "case_2_missingploygon_2" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 424349.349999999976717, 569478.400000000023283 ], [ 424349.349999999976717, 569478.400000000023283 ], [ 424346.71999999997206, 569473.63 ], [ 424375.51, 569456.900000000023283 ], [ 424379.14000000001397, 569471.949999999953434 ], [ 424358.200000000011642, 569477.900000000023283 ], [ 424349.349999999976717, 569478.400000000023283 ] ] ] } },
{ "type": "Feature", "properties": { "test_case_name": "case_2_missingploygon_3" }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 424390.25, 569495.550000000046566 ], [ 424395.349999999976717, 569512.449999999953434 ], [ 424417.88, 569498.189999999944121 ], [ 424417.549999999988358, 569497.699999999953434 ], [ 424417.479999999981374, 569494.800000000046566 ], [ 424390.25, 569495.550000000046566 ] ] ] } }
]
}
8 changes: 8 additions & 0 deletions test_cases/test_case_3_bounding_box.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "FeatureCollection",
"name": "test_case_3_bounding_box",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:EPSG::27700" } },
"features": [
{ "type": "Feature", "properties": { "gml_id": "PREDEFINED.fid-3add56ff_191ab93c9d1_-1f00", "INSPIREID": 25809009, "LABEL": 25809009, "NATIONALCADASTRALREFERENCE": 25809009, "VALIDFROM": "2009-01-19T11:45:10.845Z", "BEGINLIFESPANVERSION": "2009-01-19T11:45:10.845Z", "width": 217.24, "height": 171.977, "area": 37360.28348, "perimeter": 778.434 }, "geometry": { "type": "Polygon", "coordinates": [ [ [ 423592.450000000011642, 568216.292999999946915 ], [ 423809.69, 568216.292999999946915 ], [ 423809.69, 568388.270000000018626 ], [ 423592.450000000011642, 568388.270000000018626 ], [ 423592.450000000011642, 568216.292999999946915 ] ] ] } }
]
}
25 changes: 25 additions & 0 deletions test_cases/test_case_4_bounding_box.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"type": "FeatureCollection",
"name": "test_case_4_bounding_box",
"crs": {
"type": "name",
"properties": { "name": "urn:ogc:def:crs:EPSG::27700" }
},
"features": [
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[423263, 568241],
[423623, 568241],
[423623, 568456],
[423263, 568456],
[423263, 568241]
]
]
}
}
]
}
Loading