Skip to content

Commit

Permalink
Merge pull request #20 from simbli/dev
Browse files Browse the repository at this point in the history
Release v3.0.1
  • Loading branch information
simbli authored Feb 18, 2024
2 parents 21bb427 + e3fe490 commit 0df5d4d
Show file tree
Hide file tree
Showing 39 changed files with 734 additions and 644 deletions.
14 changes: 0 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,6 @@ Python-based API for Electronic Navigational Charts (ENC)
This module follows the [PEP8](https://www.python.org/dev/peps/pep-0008/)
convention for Python code.

## Roadmap

- 1: Add better compatibility for all operating systems (Windows, Linux++). Right
now, GDAL and Cartopy are problematic to install on most platforms.
Consider finding other packages for map loading and charts projections.
- 2: Add support for multiple map data formats (.gis, .gdb, .json) from any region in
the world, in all UTM zones or lat/lon coordinates.
- 3: Use another plotting framework that has higher refresh rate or is feasible for
real-time (Qt?, React?).
- 4: Add options for plotting trajectories, ships, traffic information/AIS data etc.
on the frontend display.
- 5: Add support for reading and loading in weather data (wind and current
maps++) from a separate module.


## Prerequisites

Expand Down
5 changes: 4 additions & 1 deletion seacharts/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
"""
Contains and exposes the ENC class and its Config class for the maritime spatial API.
"""
from .core import Config
from .enc import ENC
from .utils.config import Config
8 changes: 8 additions & 0 deletions seacharts/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""
Contains core classes and functions used throughout the SeaCharts package.
"""
from . import files
from . import paths
from .config import Config
from .parser import DataParser
from .scope import Scope
5 changes: 4 additions & 1 deletion seacharts/utils/config.py → seacharts/core/config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
Contains the Config class for ENC configuration settings.
"""
from pathlib import Path

import yaml
Expand All @@ -9,7 +12,7 @@

class Config:
"""
Class for maintaining Electronic Navigational Charts configuration settings
Class for maintaining Electronic Navigational Charts configuration settings.
"""

def __init__(self, config_path: Path | str = None):
Expand Down
5 changes: 3 additions & 2 deletions seacharts/environment/extent.py → seacharts/core/extent.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from dataclasses import dataclass
"""
Contains the Extent class for defining the span of spatial data.
"""


@dataclass
class Extent:
def __init__(self, settings: dict):
self.size = tuple(settings["enc"].get("size", (0, 0)))
Expand Down
3 changes: 1 addition & 2 deletions seacharts/utils/files.py → seacharts/core/files.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""
Contains file/directory-related utility functions, such as functions for
writing to csv files.
Contains utility functions related to system files and directories.
"""
import csv
from collections.abc import Generator
Expand Down
164 changes: 164 additions & 0 deletions seacharts/core/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""
Contains the DataParser class for spatial data parsing.
"""
import time
import warnings
from pathlib import Path
from typing import Generator

import fiona

from seacharts.core import paths
from seacharts.layers import labels, Layer


class DataParser:
def __init__(
self,
bounding_box: tuple[int, int, int, int],
path_strings: list[str],
):
self.bounding_box = bounding_box
self.paths = set([p.resolve() for p in (map(Path, path_strings))])
self.paths.update(paths.default_resources)

def load_shapefiles(self, layers: list[Layer]) -> None:
for layer in layers:
records = list(self._read_shapefile(layer.label))
layer.records_as_geometry(records)

def parse_resources(
self,
regions_list: list[Layer],
resources: list[str],
area: float
) -> None:
if not list(self._gdb_paths):
resources = sorted(list(set(resources)))
if not resources:
print("WARNING: No spatial data source location given in config.")
else:
message = "WARNING: No spatial data sources were located in\n"
message += " "
resources = [f"'{r}'" for r in resources]
message += ", ".join(resources[:-1])
if len(resources) > 1:
message += f" and {resources[-1]}"
print(message + ".")
return
print("INFO: Updating ENC with data from available resources...\n")
print(f"Processing {area // 10 ** 6} km^2 of ENC features:")
for regions in regions_list:
start_time = time.time()
records = self._load_from_fgdb(regions)
info = f"{len(records)} {regions.name} geometries"

if not records:
print(f"\rFound {info}.")
return
else:
print(f"\rMerging {info}...", end="")
regions.unify(records)

print(f"\rSimplifying {info}...", end="")
regions.simplify(0)

print(f"\rBuffering {info}...", end="")
regions.buffer(0)

print(f"\rClipping {info}...", end="")
regions.clip(self.bounding_box)

self._write_to_shapefile(regions)
end_time = round(time.time() - start_time, 1)
print(f"\rSaved {info} to shapefile in {end_time} s.")

@property
def _gdb_paths(self) -> Generator[Path, None, None]:
for path in self.paths:
if not path.is_absolute():
path = paths.cwd / path
if self._is_gdb(path):
yield path
elif path.is_dir():
for p in path.iterdir():
if self._is_gdb(p):
yield p

def _load_from_fgdb(self, layer: Layer) -> list[dict]:
depth = layer.depth if hasattr(layer, "depth") else 0
external_labels = labels.NORWEGIAN_LABELS[layer.__class__.__name__]
return list(self._read_fgdb(layer.label, external_labels, depth))

def _parse_layers(
self, path: Path, external_labels: list[str], depth: int
) -> Generator:
for label in external_labels:
if isinstance(label, dict):
layer, depth_label = label["layer"], label["depth"]
records = self._read_spatial_file(path, layer=layer)
for record in records:
if record["properties"][depth_label] >= depth:
yield record
else:
yield from self._read_spatial_file(path, layer=label)

def _read_fgdb(
self, name: str, external_labels: list[str], depth: int
) -> Generator:
for gdb_path in self._gdb_paths:
records = self._parse_layers(gdb_path, external_labels, depth)
yield from self._parse_records(records, name)

def _read_shapefile(self, label: str) -> Generator:
file_path = self._shapefile_path(label)
if file_path.exists():
yield from self._read_spatial_file(file_path)

def _read_spatial_file(self, path: Path, **kwargs) -> Generator:
try:
with fiona.open(path, "r", **kwargs) as source:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=RuntimeWarning)
for record in source.filter(bbox=self.bounding_box):
yield record
except ValueError as e:
message = str(e)
if "Null layer: " in message:
message = f"Warning: {message[12:]} not found in data set."
print(message)
return

def _shapefile_writer(self, file_path, geometry_type):
return fiona.open(
file_path,
"w",
schema=self._as_record("int", geometry_type),
driver="ESRI Shapefile",
crs={"init": "epsg:25833"},
)

def _write_to_shapefile(self, regions: Layer):
geometry = regions.mapping
file_path = self._shapefile_path(regions.label)
with self._shapefile_writer(file_path, geometry["type"]) as sink:
sink.write(self._as_record(regions.depth, geometry))

@staticmethod
def _as_record(depth, geometry):
return {"properties": {"depth": depth}, "geometry": geometry}

@staticmethod
def _is_gdb(path: Path) -> bool:
return path.is_dir() and path.suffix == ".gdb"

@staticmethod
def _parse_records(records, name):
for i, record in enumerate(records):
print(f"\rNumber of {name} records read: {i + 1}", end="")
yield record
return

@staticmethod
def _shapefile_path(label):
return paths.shapefiles / label / (label + ".shp")
2 changes: 1 addition & 1 deletion seacharts/utils/paths.py → seacharts/core/paths.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Contains hard-coded paths to relevant files and directories
Contains hard-coded paths to relevant files and directories.
"""
from pathlib import Path

Expand Down
8 changes: 5 additions & 3 deletions seacharts/environment/scope.py → seacharts/core/scope.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""
Contains the Extent class for defining details related to files of spatial data.
"""
from dataclasses import dataclass

import seacharts.utils as utils
from seacharts.core import files
from .extent import Extent


Expand All @@ -12,8 +15,7 @@ def __init__(self, settings: dict):
self.extent = Extent(settings)
self.depths = settings["enc"].get("depths", self.default_depths)
self.resources = settings["enc"].get("resources", [])
self.parser = utils.ShapefileParser(self.extent.bbox, self.resources)
self.features = ["land", "shore"]
for depth in self.depths:
self.features.append(f"seabed{depth}m")
utils.files.build_directory_structure(self.features, self.resources)
files.build_directory_structure(self.features, self.resources)
3 changes: 3 additions & 0 deletions seacharts/display/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
"""
Contains the Display class for displaying and plotting maritime spatial data.
"""
from .display import Display
13 changes: 8 additions & 5 deletions seacharts/display/colors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
Contains functions and structures for color management.
"""
import matplotlib.colors as clr
import matplotlib.pyplot as plt
import numpy as np
Expand Down Expand Up @@ -43,9 +46,9 @@ def _greens(bins: int = 9) -> np.ndarray:
)

_layer_colors = dict(
seabed=_blues()[0],
land=_greens()[4],
shore=_greens()[3],
Seabed=_blues()[0],
Land=_greens()[4],
Shore=_greens()[3],
highlight=("#ffffff44", "#ffffff44"),
blank=("#ffffffff", "#ffffffff"),
)
Expand All @@ -69,10 +72,10 @@ def color_picker(name: str, bins: int = None) -> tuple:
def colorbar(axes: Axes, depths: list[int]) -> Colorbar:
depths = list(depths)
ocean = list(_blues(len(depths)))
colors = [_layer_colors["shore"]] + ocean[:-1]
colors = [_layer_colors["Shore"]] + ocean[:-1]
c_map = clr.LinearSegmentedColormap.from_list("Custom terrain", colors, len(colors))
c_map.set_over(ocean[-1])
c_map.set_under(_layer_colors["land"])
c_map.set_under(_layer_colors["Land"])
norm = clr.BoundaryNorm([0] + depths[1:], c_map.N)
kwargs = dict(
extend="both",
Expand Down
5 changes: 3 additions & 2 deletions seacharts/display/display.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

"""
Contains the Display class for displaying and plotting maritime spatial data.
"""
import tkinter as tk
from pathlib import Path
from typing import Any
Expand Down
3 changes: 3 additions & 0 deletions seacharts/display/events.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""
Contains the EventsManager class for managing display interaction events.
"""
from typing import Any

import matplotlib.pyplot as plt
Expand Down
Loading

0 comments on commit 0df5d4d

Please sign in to comment.