Repository navigation
160 rotating coords #163
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
Open
alasdair-roy
wants to merge
29
commits into
MetOffice:main
Choose a base branch
from
alasdair-roy:160_rotating_coords
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
160 rotating coords #163
Changes from 15 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
e47273e
Placed sketch solution for rotating co-ordinate systems
alasdair-roy ca534fb
Allow points to be transformed to a rotated pole.
alasdair-roy 2d8879e
Testing with optional argument in rose-stem.
alasdair-roy 36a3819
Added source cube as argument, added some tests for shapefile creation.
alasdair-roy a6a60eb
Added more tests
alasdair-roy 64af015
Changed source cube to optional and cleaned tests
alasdair-roy 5eed8dd
Remove some comments that are not needed
alasdair-roy 4448d86
Improved comments in tests.
alasdair-roy 5d1a68a
Fixed issue causing polygon to invert
alasdair-roy af32edf
Moved functionality out of load polygon to ease testing.
alasdair-roy fa4bbeb
Minor reformatting of docstrings.
alasdair-roy 791118e
Added docstrings to tests.
alasdair-roy 4ad7412
Changed some mocking in tests to improve readability.
alasdair-roy 5ea3e01
Corrected spelling mistakes.
alasdair-roy c848fb5
Moved loading out of function to make testing easier.
alasdair-roy 5f07ac1
Moved transformation out of polygon loading.
alasdair-roy 52dd428
Add code to address antimeridian crossing.
alasdair-roy 8f9799c
Fixed using wrong points for antimeridian crossing.
alasdair-roy 2cc209b
Changed variable names to correct long misname.
alasdair-roy 2534d8b
Removed double negative.
alasdair-roy 362d653
Added extra checks and tests for polygon validity and orientation.
alasdair-roy 368b878
Renamed polygon variable name in test to match orientation.
alasdair-roy e1d61ea
Addressed review on readability and docstrings.
alasdair-roy 8e9ae5b
Added integration test for unrotated ITE.
alasdair-roy b76caf7
Added new ite test and made other tests more consistent.
alasdair-roy 12f4707
Added warning if the polygon lands outside of the local domain.
alasdair-roy 613372e
Added polygon domain warning and added json metadata file.
alasdair-roy c0cc24f
Minor format changes.
alasdair-roy 4b68e18
Added comment on polygon orientation.
alasdair-roy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,53 +9,276 @@ | |
|
|
||
| Creates and saves a shapefile from a list of pairs of longitude, latitude | ||
| points defining a single polygon in a specified polygon file. | ||
|
|
||
| Rotated pole domains can be specified using the land sea mask argument, | ||
| where the longitude, latitude pairs are rotated to the new pole. | ||
| Unless a source cube is provided with the source coordinate reference system, | ||
| it assumed that the points defined in the json file are on a standard spherical | ||
| unrotated geodetic coordinate reference system. | ||
| """ | ||
| import argparse | ||
| import json | ||
| import logging | ||
| import warnings | ||
|
|
||
| import ants | ||
| import iris.coord_systems | ||
| import numpy as np | ||
| from ants.io.load import load_cube, load_landsea_mask | ||
| from ants.utils.cube import CubeBuilder | ||
| from osgeo import ogr | ||
| from shapely.geometry import Polygon | ||
|
|
||
| _LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def _check_coord_system_type(target_lsm): | ||
| """ | ||
| Check that target_lsm has a rotated pole coordinate system. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| target_lsm : :class:`iris.cube.Cube` | ||
| The lsm cube specifiying the rotated pole coordinate system. | ||
|
|
||
| Raises | ||
| ------ | ||
| ValueError : | ||
| If target_lsm does not have a rotated pole coordinate system. | ||
| """ | ||
|
|
||
| is_pole_coords = isinstance( | ||
| target_lsm.coord_system(), iris.coord_systems.RotatedGeogCS | ||
| ) | ||
| if not is_pole_coords: | ||
| raise ValueError( | ||
| f"target_lsm.coord_system() {target_lsm.coord_system()} is not" | ||
| f" an instance of {iris.coord_systems.RotatedGeogCS}." | ||
| f" The landsea mask should specify a valid rotated pole coordinate" | ||
| f" system." | ||
| ) | ||
|
|
||
|
|
||
| def _validate_orientation(target_lsm): | ||
| """ | ||
| Validate that the rotated pole coordinate system is not coincident with the | ||
| geographic north pole. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| target_lsm : :class:`iris.cube.Cube` | ||
| The target lsm cube specifying the rotated pole coordinate system. | ||
|
|
||
| Returns | ||
| ------- | ||
| bool | ||
| True if target_lsm is a valid rotated pole, False otherwise. | ||
|
|
||
| Warns | ||
| ----- | ||
| UserWarning | ||
| If the rotated pole is coincident with the geographic north pole. | ||
| """ | ||
|
|
||
| target_crs = target_lsm.coord_system() | ||
|
|
||
| grid_lon = target_crs.grid_north_pole_longitude | ||
| grid_lat = target_crs.grid_north_pole_latitude | ||
| pole_lon_rotation = target_crs.north_pole_grid_longitude | ||
|
|
||
| valid_crs = not ( | ||
| ants.utils.ndarray.allclose( | ||
| [grid_lon, grid_lat, pole_lon_rotation], [0.0, 90.0, 0.0] | ||
| ) | ||
| ) | ||
| if not valid_crs: | ||
| warnings.warn( | ||
| "target_lsm has a geodetic coordinate system with pole located" | ||
| f"at grid_longitude={grid_lon}, grid_latitude={grid_lat}." | ||
| "No transformation will be carried out." | ||
| ) | ||
| return valid_crs | ||
|
|
||
|
|
||
| def _validate_args(target_lsm_path, source_cube_path): | ||
| if source_cube_path is not None and target_lsm_path is None: | ||
| raise ValueError( | ||
| "If --source-cube is passed then --target-lsm must also be given." | ||
| ) | ||
|
|
||
|
|
||
| def _transform_coordinates(target_lsm, source_cube, points): | ||
| """ | ||
| Transform the longitude-latitude points to the rotated pole. | ||
|
|
||
| The source coordinate system is assumed to be unrotated geodetic defined | ||
| on a sphere, unless otherwise specified. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| target_lsm : :class:`iris.cube.Cube` | ||
| The lsm cube specifying the rotated pole coordinate system. | ||
| source_cube : :class:`iris.cube.Cube` | ||
| An iris cube specifying the coordinate system of the input json | ||
| file. | ||
| points : :class:`numpy.ndarray` | ||
| An ``(m, 2)`` numpy array of m longitude-latitude pairs. | ||
|
|
||
| Returns | ||
| ------- | ||
| : :class:`numpy.ndarray` | ||
| An ``(m, 2)`` numpy array of transformed longitude-latitude pairs. | ||
| """ | ||
|
|
||
| source_crs = source_cube.coord_system().as_cartopy_crs() | ||
| target_coord = target_lsm.coord_system() | ||
| target_crs = target_coord.as_cartopy_crs() | ||
|
|
||
| rotated_points = target_crs.transform_points( | ||
| source_crs, points[:, 0], points[:, 1] | ||
| )[:, :2] | ||
|
|
||
| _LOGGER.info( | ||
| "Input json file transformed to new pole rotated coordinate system at" | ||
| "pole longitude=%s, pole latitude=%s, central rotated longitude=%s.", | ||
| target_coord.grid_north_pole_longitude, | ||
| target_coord.grid_north_pole_latitude, | ||
| target_coord.north_pole_grid_longitude, | ||
| ) | ||
|
|
||
| return rotated_points | ||
|
|
||
|
|
||
| def _load_cubes(target_lsm_path, source_cube_path): | ||
| """ | ||
| Load the target lsm and create a source cube if not provided. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| target_lsm_path : str | ||
| File path to a land sea mask that provides the new rotated pole. | ||
| source_cube_path : str | ||
| File path to an iris cube specifying the coordinate system of the | ||
| input json file. | ||
|
|
||
| Returns | ||
| ------- | ||
| : tuple(:class:`iris.cube.Cube`, :class:`iris.cube.Cube`) | ||
| A tuple containing the target lsm and the source cube respectively. | ||
| """ | ||
|
|
||
| target_lsm = load_landsea_mask(target_lsm_path) | ||
|
|
||
| if source_cube_path is None: | ||
| crs = iris.coord_systems.GeogCS(6371229.0) | ||
| source_cube = CubeBuilder(crs, (2, 2))._cube | ||
| else: | ||
| source_cube = load_cube(source_cube_path) | ||
|
|
||
| return target_lsm, source_cube | ||
|
|
||
| def _load_polygon_from_json(json_file): | ||
|
|
||
| def _transform_if_required(target_lsm_path, source_cube_path, points): | ||
| """ | ||
| Load a json file containing a list of pairs of longitude, latitude points | ||
| Perform the transformation to a rotated pole if the coordinate system | ||
| is valid. | ||
|
|
||
| The points are transformed to the specified rotated pole coordinate system | ||
| if the provided coordinate system is a rotated pole and is not coincident | ||
| with the geographic north pole. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| target_lsm_path : str | ||
| File path to a land sea mask that provides the new rotated pole. | ||
| source_cube_path : str | ||
| File path to an iris cube specifying the coordinate system of the | ||
| input json file. | ||
| points : :class:`numpy.ndarray` | ||
| An ``(m, 2)`` numpy array of m longitude-latitude pairs. | ||
|
|
||
| Returns | ||
| ------- | ||
| : :class:`numpy.ndarray` | ||
| An ``(m, 2)`` numpy array of transformed longitude-latitude pairs. | ||
| """ | ||
|
|
||
| target_lsm, source_cube = _load_cubes(target_lsm_path, source_cube_path) | ||
|
|
||
| _check_coord_system_type(target_lsm) | ||
|
|
||
| if _validate_orientation(target_lsm): | ||
| rotated_points = _transform_coordinates(target_lsm, source_cube, points) | ||
| else: | ||
| rotated_points = np.copy(points) | ||
|
|
||
| return rotated_points | ||
|
|
||
|
|
||
| def _load_polygon_from_json(json_file, target_lsm_path, source_cube_path): | ||
|
jrackham-mo marked this conversation as resolved.
Outdated
|
||
| """ | ||
| Load a json file containing a list of pairs of longitude-latitude points | ||
| to create a polygon from. | ||
|
|
||
| If a valid rotated pole is specifed in the target lsm, first transforms | ||
| the points from the source coordinate system to the rotated pole before | ||
| creating a polygon. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| json_file : str | ||
| Path to json file | ||
| target_lsm_path : str | ||
| File path to a land sea mask that provides the new rotated pole. | ||
| source_cube_path : str | ||
| File path to an iris cube specifying the coordinate system of the | ||
| input json file. | ||
|
|
||
| Returns | ||
| ------- | ||
| : :class:`~shapely.geometry.Polygon` | ||
| """ | ||
|
|
||
| with open(json_file, "r") as polygon_json: | ||
| polygon = json.load(polygon_json) | ||
| polygon = np.array(polygon) | ||
| polygon = Polygon(polygon) | ||
| points = json.load(polygon_json) | ||
| points = np.array(points) | ||
|
|
||
| if target_lsm_path is not None: | ||
| points = _transform_if_required(target_lsm_path, source_cube_path, points) | ||
|
|
||
| polygon = Polygon(points) | ||
|
|
||
| return polygon | ||
|
|
||
|
|
||
| def main(json_file, output): | ||
| def main(json_file, output, target_lsm_path, source_cube_path): | ||
| """ | ||
| Create a shape file from pairs of longitude, latitude points. | ||
|
|
||
| Loads in a provided json file that defines pairs of longitude, latitude | ||
| points to create a polygon from. That polygon is then used to create a | ||
| shape file that is saved to the specified output location. | ||
|
|
||
| If target_lsm_path is provided, the points are first transformed from an | ||
| source geodetic coordinate system to a rotated pole coordinate system | ||
| specified by the lsm. It is assumed that the points in the json | ||
| file are on an unrotated geodetic grid, unless otherwise specified. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| json_file : str | ||
| Path to json file | ||
| output : str | ||
| Location to store generated shape file | ||
|
|
||
| target_lsm_path : str | ||
| File path to a land sea mask that provides the new rotated pole. | ||
| source_cube_path : str | ||
| File path to an iris cube specifying the coordinate system of the | ||
| input json file. | ||
| """ | ||
|
|
||
| # Load a json and make a polygon | ||
| polygon = _load_polygon_from_json(json_file) | ||
| polygon = _load_polygon_from_json(json_file, target_lsm_path, source_cube_path) | ||
|
|
||
| # Now convert it to a shapefile with OGR | ||
| driver = ogr.GetDriverByName("Esri Shapefile") | ||
|
|
@@ -87,13 +310,29 @@ def _get_parser(): | |
| "json_file", help="Path to json file defining polygon to generate." | ||
| ) | ||
| parser.add_argument("output", help="File to save shape file to.") | ||
| parser.add_argument( | ||
|
jrackham-mo marked this conversation as resolved.
|
||
| "--target-lsm", | ||
| type=ants.config.filepath_readable, | ||
| required=False, | ||
| help="Path to the land sea mask containing the rotated pole" | ||
| " coordinate system.", | ||
| ) | ||
| parser.add_argument( | ||
| "--source-cube", | ||
| type=ants.config.filepath_readable, | ||
| required=False, | ||
| help="Path to an iris cube which specifies the co-ordinate" | ||
| " system of the json file.", | ||
| ) | ||
|
Comment on lines
+421
to
+426
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be more appropriate to call this |
||
| return parser | ||
|
|
||
|
|
||
| def cli_interface(): | ||
| parser = _get_parser() | ||
| args = parser.parse_args() | ||
| main(args.json_file, args.output) | ||
|
|
||
| _validate_args(args.target_lsm, args.source_cube) | ||
| main(args.json_file, args.output, args.target_lsm, args.source_cube) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # (C) Crown Copyright, Met Office. All rights reserved. | ||
| # | ||
| # This file is part of ANTS and is released under the BSD 3-Clause license. | ||
| # See LICENSE.txt in the root of the repository for full licensing details. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.