Source code for gfinder.utils

import spiceypy as spice
import numpy as np
from geojson import Polygon, Feature, FeatureCollection
import geojson


[docs]def lon360to180(lon): new_lon = lon % 360 new_lon = new_lon - 360 if new_lon > 180 else new_lon return new_lon
[docs]def roi_pnt2vtx(center_lon, center_lat, diameter, body_radius, n_vertices): vertex_coords = [] # ROI center point pointing (normalised) vector in body-fixed frame p_vec = spice.latrec(1.0, lon360to180(center_lon)*spice.rpd(), center_lat*spice.rpd()) # ROI angular diameter theta = diameter / ( np.cos(center_lat*spice.rpd()) * body_radius ) # fist vertex (rotation of p_vec around +Z axis of body-fixed frame by theta/2 angle) vertex = spice.vrotv(p_vec, [0,0,1], theta/2) r, lon, lat = spice.reclat(vertex) vertex_coords = [ ( lon*spice.dpr(), lat*spice.dpr() ) ] # define vertices rotation angles around p_vec alphas = [ (i+1)*(spice.twopi()/n_vertices) for i in range(n_vertices-1)] # define following vertices for alpha in alphas: this_vertex = spice.vrotv(vertex, p_vec, alpha) r, lon, lat = spice.reclat(this_vertex) vertex_coords.append( ( lon*spice.dpr(), lat*spice.dpr() ) ) vertex_coords = [ vertex_coords + [vertex_coords[0]] ] return vertex_coords
[docs]def nomenclature_to_roi(input_geojson_file, output_geojson_file, body_radius, n_vertices): """ Turn an input nomenclature GeoJSON file containing the longitude, latitude, and diameter of each POINT feature into an output GeoJSON file containing a 'polygonized' geometry features. """ with open(input_geojson_file, 'r') as f: input_geojson = json.load(f) ids = [] center_lons = [] center_lats = [] diameters = [] for feature in input_geojson['features']: ids.append(feature['properties']['name']) center_lons.append(feature['properties']['center_lon']) center_lats.append(feature['properties']['center_lat']) diameters.append(feature['properties']['diameter']) collection = [] for id, lon, lat, diameter in zip(ids, center_lons, center_lats, diameters): vertex_coords = roi_pnt2vtx(lon, lat, diameter, body_radius, n_vertices) feature = Feature(geometry=Polygon(vertex_coords), properties={'id':id, 'diameter':diameter}) collection.append(feature) with open(output_geojson_file, 'w') as f: geojson.dump(FeatureCollection(collection), f)
[docs]def add_roi_to_geojson(geojson_file, id, center_lon, center_lat, diameter, body_radius, n_vertices): with open(geojson_file, 'r') as f: rois_geojson = geojson.load(f) # new ROI feature vertex_coords = roi_pnt2vtx(center_lon, center_lat, diameter, body_radius, n_vertices) feature = Feature(geometry=Polygon(vertex_coords), properties={'id':id, 'diameter':diameter}) features = rois_geojson['features'] features.append(feature) new_rois_geojson = FeatureCollection(features) with open(geojson_file, 'w') as fout: geojson.dump(new_rois_geojson, fout)
# -- Polygon split implementation by PawaritL # https://gist.github.com/PawaritL/ec7136c0b718ca65db6df1c33fd1bb11 # https://towardsdatascience.com/around-the-world-in-80-lines-crossing-the-antimeridian-with-python-and-shapely-c87c9b6e1513 from typing import Union, List, Generator from shapely.geometry import mapping, Polygon, GeometryCollection from shapely import affinity
[docs]def check_crossing(lon1: float, lon2: float, validate: bool = False, dlon_threshold: float = 180.0): """ Assuming a minimum travel distance between two provided longitude coordinates, checks if the 180th meridian (antimeridian) is crossed. """ if validate and any([abs(x) > 180.0 for x in [lon1, lon2]]): raise ValueError("longitudes must be in degrees [-180.0, 180.0]") return abs(lon2 - lon1) > dlon_threshold
[docs]def translate_polygons(geometry_collection: GeometryCollection, output_format: str = "geojson") -> Generator[ Union[List[dict], List[Polygon]], None, None ]: for polygon in geometry_collection.geoms: # added .geoms (minx, _, maxx, _) = polygon.bounds if minx < -180: geo_polygon = affinity.translate(polygon, xoff=360) elif maxx > 180: geo_polygon = affinity.translate(polygon, xoff=-360) else: geo_polygon = polygon yield_geojson = output_format == "geojson" yield json.dumps(mapping(geo_polygon)) if (yield_geojson) else geo_polygon
import math import copy import json from typing import Union, List from shapely.geometry import Polygon, LineString, GeometryCollection from shapely.ops import split
[docs]def split_polygon(geojson: dict, output_format: str = "geojson", validate: bool = False) -> Union[ List[dict], List[Polygon], GeometryCollection ]: """ Given a GeoJSON representation of a Polygon, returns a collection of 'antimeridian-safe' constituent polygons split at the 180th meridian, ensuring compliance with GeoJSON standards (https://tools.ietf.org/html/rfc7946#section-3.1.9) Assumptions: - Any two consecutive points with over 180 degrees difference in longitude are assumed to cross the antimeridian - The polygon spans less than 360 degrees in longitude (i.e. does not wrap around the globe) - However, the polygon may cross the antimeridian on multiple occasions Parameters: geojson (dict): GeoJSON of input polygon to be split. For example: { "type": "Polygon", "coordinates": [ [ [179.0, 0.0], [-179.0, 0.0], [-179.0, 1.0], [179.0, 1.0], [179.0, 0.0] ] ] } output_format (str): Available options: "geojson", "polygons", "geometrycollection" If "geometrycollection" returns a Shapely GeometryCollection. Otherwise, returns a list of either GeoJSONs or Shapely Polygons validate (bool): Checks if all longitudes are within [-180.0, 180.0] Returns: List[dict]/List[Polygon]/GeometryCollection: antimeridian-safe polygon(s) """ output_format = output_format.replace("-", "").strip().lower() coords_shift = copy.deepcopy(geojson["coordinates"]) shell_minx = shell_maxx = None split_meridians = set() splitter = None for ring_index, ring in enumerate(coords_shift): if len(ring) < 1: continue else: ring_minx = ring_maxx = ring[0][0] crossings = 0 for coord_index, (lon, _) in enumerate(ring[1:], start=1): lon_prev = ring[coord_index - 1][0] # [0] corresponds to longitude coordinate if check_crossing(lon, lon_prev, validate=validate): direction = math.copysign(1, lon - lon_prev) coords_shift[ring_index][coord_index][0] = lon - (direction * 360.0) crossings += 1 x_shift = coords_shift[ring_index][coord_index][0] if x_shift < ring_minx: ring_minx = x_shift if x_shift > ring_maxx: ring_maxx = x_shift # Ensure that any holes remain contained within the (translated) outer shell if (ring_index == 0): # by GeoJSON definition, first ring is the outer shell shell_minx, shell_maxx = (ring_minx, ring_maxx) elif (ring_minx < shell_minx): ring_shift = [[x + 360, y] for (x, y) in coords_shift[ring_index]] coords_shift[ring_index] = ring_shift ring_minx, ring_maxx = (x + 360 for x in (ring_minx, ring_maxx)) elif (ring_maxx > shell_maxx): ring_shift = [[x - 360, y] for (x, y) in coords_shift[ring_index]] coords_shift[ring_index] = ring_shift ring_minx, ring_maxx = (x - 360 for x in (ring_minx, ring_maxx)) if crossings: # keep track of meridians to split on if ring_minx < -180: split_meridians.add(-180) if ring_maxx > 180: split_meridians.add(180) n_splits = len(split_meridians) if n_splits > 1: raise NotImplementedError( """Splitting a Polygon by multiple meridians (MultiLineString) not supported by Shapely""" ) elif n_splits == 1: split_lon = next(iter(split_meridians)) meridian = [[split_lon, -90.0], [split_lon, 90.0]] splitter = LineString(meridian) shell, *holes = coords_shift if splitter else geojson["coordinates"] if splitter: split_polygons = split(Polygon(shell, holes), splitter) else: split_polygons = GeometryCollection([Polygon(shell, holes)]) geo_polygons = list(translate_polygons(split_polygons, output_format)) if output_format == "geometrycollection": return GeometryCollection(geo_polygons) else: return geo_polygons
import os from gfinder.geometry import GeometryFactory
[docs]def autoclass_content(module_name, class_name): content = class_name + '\n' content += '^' * len(class_name) + '\n' content += '\n' content += f'.. autoclass:: gfinder.{module_name}.{class_name}\n' content += ' :members:\n' content += ' :undoc-members:\n' content += ' :show-inheritance:\n' content += '\n' return content
[docs]def gen_apidocs(dirpath): modules = { 'config': 'Config', 'datastore': 'DataStore', 'dataviewer': 'DataViewer', 'detector': 'Detector', 'event': 'Event', 'geometry': 'Geometry', 'geometryevent': 'GeometryEvent', 'opportunity': 'Opportunity', 'radiometry': 'Radiometry', 'resource': 'Resource', 'roi': 'ROI', 'scenario': 'Scenario', 'simulator': 'Simulator', 'tamn': 'TAMN', 'target': 'Target', 'timeline': 'Timeline', 'utils': 'Utils' } geometry_factory = GeometryFactory() core_geometry_classes = ['Geometry', 'Condition', 'GeometryFactory'] lowlevel_class_names = geometry_factory.lowlevel_class_names highlevel_class_names = geometry_factory.highlevel_class_names excluded_class_names = core_geometry_classes + lowlevel_class_names + highlevel_class_names all_geometries = geometry_factory.get_geometries() all_geometries.sort() for key in modules.keys(): title = f'``{modules[key]}`` module' content = f'.. _ref_gfinder_{key}_module:\n' content += '\n' content += f'{title}\n' content += '=' * len(title) + '\n' content += '\n' if key == 'geometry': content += 'Core classes\n' content += '------------\n' content += '\n' for class_name in core_geometry_classes: content += autoclass_content(key, class_name) content += 'Low-level Geometry classes\n' content += '--------------------------\n' content += '\n' for class_name in geometry_factory.lowlevel_class_names: content += autoclass_content(key, class_name) content += 'High-level Geometry classes\n' content += '---------------------------\n' content += '\n' for class_name in geometry_factory.highlevel_class_names: content += autoclass_content(key, class_name) content += 'Geometry classes\n' content += '----------------\n' content += '\n' for class_name in all_geometries: if class_name not in excluded_class_names: content += autoclass_content(key, class_name) else: content += f'.. automodule:: gfinder.{key}\n' content += ' :members:\n' content += ' :undoc-members:\n' content += ' :show-inheritance:' # delete rst file if exists rstfile = f'{dirpath}/{key}.rst' if os.path.exists(rstfile): os.remove(rstfile) print(f'removed {rstfile} file.') # write rst file with open(rstfile, 'w') as f: f.write(content) print(f'written {rstfile} file.')