Polyline/polygon files¶
Polyline .pli(z) and polygon .pol files are basic spatial input files for a D-Flow FM model to select particular locations, and are used in various other input files.
They are represented by the classes below.
Model¶
models.py defines all classes and functions related to representing pol/pli(z) files.
Description (BaseModel)
pydantic-model
¶
Description of a single PolyObject.
The Description will be prepended to a block. Each line will start with a '*'.
Attributes:
Name | Type | Description |
---|---|---|
content |
str |
The content of this Description. |
Metadata (BaseModel)
pydantic-model
¶
Metadata of a single PolyObject.
Attributes:
Name | Type | Description |
---|---|---|
name |
str |
The name of the PolyObject |
n_rows |
int |
The number of rows (i.e. Point instances) of the PolyObject |
n_columns |
int |
The total number of values in a Point, including x, y, and z. |
Point (BaseModel)
pydantic-model
¶
Point consisting of a x and y coordinate, an optional z coordinate and data.
Attributes:
Name | Type | Description |
---|---|---|
x |
float |
The x-coordinate of this Point |
y |
float |
The y-coordinate of this Point |
z |
Optional[float] |
An optional z-coordinate of this Point. |
data |
Sequence[float] |
The additional data variables of this Point. |
PolyObject (BaseModel)
pydantic-model
¶
PolyObject describing a single block in a poly file.
The metadata should be consistent with the points: - The number of points should be equal to number of rows defined in the metadata - The data of each point should be equal to the number of columns defined in the metadata.
Attributes:
Name | Type | Description |
---|---|---|
description |
Optional[Description] |
An optional description of this PolyObject |
metadata |
Metadata |
The Metadata of this PolObject, describing the structure |
points |
List[Point] |
The points describing this PolyObject, structured according to the Metadata |
Parser¶
parser.py defines all classes and functions related to parsing pol/pli(z) files.
Block (BaseModel)
pydantic-model
¶
Block is a temporary object which will be converted into a PolyObject.
The fields are supposed to be set during the lifetime of this object. When all fields are set, finalize can be called.
Attributes:
Name | Type | Description |
---|---|---|
start_line |
int |
The starting line of this current block. |
name |
Optional[str] |
The name of this block. Defaults to None. |
dimensions |
Optional[Tuple[int, int]] |
The dimensions (n_rows, n_columns) of this Block. Defaults to None. |
points |
Optional[List[Point]] |
The points of this block. Defaults to None. |
ws_warnings |
List[ParseMsg] |
The whitespace warnings associated with this block. Defaults to an empty list. |
empty_lines |
List[int] |
The line numbers of the empty lines. Defaults to an empty list. |
finalize(self) -> Optional[Tuple[hydrolib.core.io.polyfile.models.PolyObject, List[hydrolib.core.io.polyfile.parser.ParseMsg]]]
¶
Finalise this Block and return the constructed PolyObject and warnings
If the metadata or the points are None, then None is returned.
Returns:
Type | Description |
---|---|
Optional[Tuple[PolyObject, List[ParseMsg]]] |
The constructed PolyObject and warnings encountered while parsing it. |
Source code in hydrolib/core/io/polyfile/parser.py
def finalize(self) -> Optional[Tuple[PolyObject, List[ParseMsg]]]:
"""Finalise this Block and return the constructed PolyObject and warnings
If the metadata or the points are None, then None is returned.
Returns:
Optional[Tuple[PolyObject, List[ParseMsg]]]:
The constructed PolyObject and warnings encountered while parsing it.
"""
metadata = self._get_metadata()
if metadata is None or self.points is None:
return None
obj = PolyObject(
description=self._get_description(), metadata=metadata, points=self.points
)
return obj, self.ws_warnings + self._get_empty_line_warnings()
ErrorBuilder
¶
ErrorBuilder provides the functionality to the Parser to keep track of errors.
__init__(self) -> None
special
¶
Create a new ErorrorBuilder
Source code in hydrolib/core/io/polyfile/parser.py
def __init__(self) -> None:
"""Create a new ErorrorBuilder"""
self._current_block: Optional[InvalidBlock] = None
end_invalid_block(self, line: int) -> None
¶
Store the end line of the current block
If no invalid block currently exists, nothing will be done.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
line |
int |
the final line of this invalid block |
required |
Source code in hydrolib/core/io/polyfile/parser.py
def end_invalid_block(self, line: int) -> None:
"""Store the end line of the current block
If no invalid block currently exists, nothing will be done.
Args:
line (int): the final line of this invalid block
"""
if self._current_block is not None:
self._current_block.end_line = line
finalize_previous_error(self) -> Optional[hydrolib.core.io.polyfile.parser.ParseMsg]
¶
Finalize the current invalid block if it exists
If no current invalid block exists, None will be returned, and nothing will change. If a current block exists, it will be converted into a ParseMsg and returned. The current invalid block will be reset.
Returns:
Type | Description |
---|---|
Optional[ParseMsg] |
The corresponding ParseMsg if an InvalidBlock exists. |
Source code in hydrolib/core/io/polyfile/parser.py
def finalize_previous_error(self) -> Optional[ParseMsg]:
"""Finalize the current invalid block if it exists
If no current invalid block exists, None will be returned, and nothing will
change. If a current block exists, it will be converted into a ParseMsg and
returned. The current invalid block will be reset.
Returns:
Optional[ParseMsg]: The corresponding ParseMsg if an InvalidBlock exists.
"""
if self._current_block is not None:
msg = self._current_block.to_msg()
self._current_block = None
return msg
else:
return None
start_invalid_block(self, block_start: int, invalid_line: int, reason: str) -> None
¶
Start a new invalid block if none exists at the moment
If we are already in an invalid block, or the previous one was never finalised, we will not log the reason, and assume it is one long invalid block.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
block_start |
int |
The start of the invalid block. |
required |
invalid_line |
int |
The actual offending line number. |
required |
reason |
str |
The reason why this block is invalid. |
required |
Source code in hydrolib/core/io/polyfile/parser.py
def start_invalid_block(
self, block_start: int, invalid_line: int, reason: str
) -> None:
"""Start a new invalid block if none exists at the moment
If we are already in an invalid block, or the previous one
was never finalised, we will not log the reason, and assume
it is one long invalid block.
Args:
block_start (int): The start of the invalid block.
invalid_line (int): The actual offending line number.
reason (str): The reason why this block is invalid.
"""
if self._current_block is None:
self._current_block = InvalidBlock(
start_line=block_start, invalid_line=invalid_line, reason=reason
)
InvalidBlock (BaseModel)
pydantic-model
¶
InvalidBlock is a temporary object which will be converted into a ParseMsg.
Attributes:
Name | Type | Description |
---|---|---|
start_line |
int |
The start line of this InvalidBlock |
end_line |
Optional[int] |
The end line of this InvalidBlock if it is set. Defaults to None. |
invalid_line |
int |
The line which is causing this block to be invalid. |
reason |
str |
A human-readable string detailing the reason of the ParseMsg. |
to_msg(self) -> ParseMsg
¶
Convert this InvalidBlock to the corresponding ParseMsg
Returns:
Type | Description |
---|---|
ParseMsg |
The ParseMsg corresponding with this InvalidBlock |
Source code in hydrolib/core/io/polyfile/parser.py
def to_msg(self) -> ParseMsg:
"""Convert this InvalidBlock to the corresponding ParseMsg
Returns:
ParseMsg: The ParseMsg corresponding with this InvalidBlock
"""
return ParseMsg(
line_start=self.start_line,
line_end=self.end_line,
reason=f"{self.reason} at line {self.invalid_line}.",
)
ParseMsg (BaseModel)
pydantic-model
¶
ParseMsg defines a single message indicating a significant parse event.
Attributes:
Name | Type | Description |
---|---|---|
line_start |
int |
The start line of the block to which this ParseMsg refers. |
line_end |
int |
The end line of the block to which this ParseMsg refers. |
column |
Optional[Tuple[int, int]] |
An optional begin and end column to which this ParseMsg refers. |
reason |
str |
A human-readable string detailing the reason of the ParseMsg. |
notify_as_warning(self, file_path: Optional[pathlib.Path] = None)
¶
Call warnings.warn with a formatted string describing this ParseMsg
Parameters:
Name | Type | Description | Default |
---|---|---|---|
file_path |
Optional[Path] |
The file path mentioned in the warning if specified. Defaults to None. |
None |
Source code in hydrolib/core/io/polyfile/parser.py
def notify_as_warning(self, file_path: Optional[Path] = None):
"""Call warnings.warn with a formatted string describing this ParseMsg
Args:
file_path (Optional[Path], optional):
The file path mentioned in the warning if specified. Defaults to None.
"""
if self.line_start != self.line_end:
block_suffix = f"\nInvalid block {self.line_start}:{self.line_end}"
else:
block_suffix = f"\nInvalid line {self.line_start}"
col_suffix = (
f"\nColumns {self.column[0]}:{self.column[1]}"
if self.column is not None
else ""
)
file_suffix = f"\nFile: {file_path}" if file_path is not None else ""
warnings.warn(f"{self.reason}{block_suffix}{col_suffix}{file_suffix}")
Parser
¶
Parser provides the functionality to parse a polyfile line by line.
The Parser parses blocks describing PolyObject instances by relying on a rudimentary state machine. The states are encoded with the StateType. New lines are fed through the feed_line method. After each line the internal state will be updated. When a complete block is read, it will be converted into a PolyObject and stored internally. When finalise is called, the constructed objects, as well as any warnings and errors describing invalid blocks, will be returned.
Each state defines a feed_line method, stored in the _feed_line dict, which consumes a line and potentially transitions the state into the next. Each state further defines a finalise method, stored in the _finalise dict, which is called upon finalising the parser.
Invalid states are encoded with INVALID_STATE. In this state the Parser attempts to find a new block, and thus looks for a new description or name.
Unexpected whitespace before comments, names, and dimensions, as well as empty lines will generate a warning, and will be ignored by the parser.
__init__(self, file_path: Path, has_z_value: bool = False) -> None
special
¶
Create a new Parser
Parameters:
Name | Type | Description | Default |
---|---|---|---|
file_path |
Path |
Name of the file being parsed, only used for providing proper warnings. |
required |
has_z_value |
bool |
Whether to interpret the third column as z-coordinates. Defaults to False. |
False |
Source code in hydrolib/core/io/polyfile/parser.py
def __init__(self, file_path: Path, has_z_value: bool = False) -> None:
"""Create a new Parser
Args:
file_path (Path):
Name of the file being parsed, only used for providing proper warnings.
has_z_value (bool, optional):
Whether to interpret the third column as z-coordinates.
Defaults to False.
"""
self._has_z_value = has_z_value
self._file_path = file_path
self._line = 0
self._new_block()
self._error_builder = ErrorBuilder()
self._poly_objects: List[PolyObject] = []
self._current_point: int = 0
self._feed_line: Dict[StateType, Callable[[str], None]] = {
StateType.NEW_BLOCK: self._parse_name_or_new_description,
StateType.PARSED_DESCRIPTION: self._parse_name_or_next_description,
StateType.PARSED_NAME: self._parse_dimensions,
StateType.PARSING_POINTS: self._parse_next_point,
StateType.INVALID_STATE: self._parse_name_or_new_description,
}
self._finalise: Dict[StateType, Callable[[], None]] = {
StateType.NEW_BLOCK: self._noop,
StateType.PARSED_DESCRIPTION: self._add_current_block_as_incomplete_error,
StateType.PARSED_NAME: self._add_current_block_as_incomplete_error,
StateType.PARSING_POINTS: self._add_current_block_as_incomplete_error,
StateType.INVALID_STATE: self._noop,
}
self._handle_ws: Dict[StateType, Callable[[str], None]] = {
StateType.NEW_BLOCK: self._log_ws_warning,
StateType.PARSED_DESCRIPTION: self._log_ws_warning,
StateType.PARSED_NAME: self._log_ws_warning,
StateType.PARSING_POINTS: self._noop,
StateType.INVALID_STATE: self._noop,
}
feed_line(self, line: str) -> None
¶
Parse the next line with this Parser.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
line |
str |
The line to parse |
required |
Source code in hydrolib/core/io/polyfile/parser.py
def feed_line(self, line: str) -> None:
"""Parse the next line with this Parser.
Args:
line (str): The line to parse
"""
if not Parser._is_empty_line(line):
self._handle_ws[self._state](line)
self._feed_line[self._state](line)
else:
self._handle_empty_line()
self._increment_line()
finalize(self) -> Sequence[hydrolib.core.io.polyfile.models.PolyObject]
¶
Finalize parsing and return the constructed PolyObject.
Returns:
Type | Description |
---|---|
PolyObject |
A PolyObject containing the constructed PolyObject instances. |
Source code in hydrolib/core/io/polyfile/parser.py
def finalize(self) -> Sequence[PolyObject]:
"""Finalize parsing and return the constructed PolyObject.
Returns:
PolyObject:
A PolyObject containing the constructed PolyObject instances.
"""
self._error_builder.end_invalid_block(self._line)
last_error_msg = self._error_builder.finalize_previous_error()
if last_error_msg is not None:
self._handle_parse_msg(last_error_msg)
self._finalise[self._state]()
return self._poly_objects
StateType (IntEnum)
¶
The types of state of a Parser.
read_polyfile(filepath: Path, has_z_values: bool) -> Dict
¶
Read the specified file and return the corresponding data.
The file is expected to follow the .pli(z) / .pol convention. A .pli(z) or .pol file is defined as consisting of a number of blocks of lines adhering to the following format:
- Optional description record consisting of one or more lines starting with '*'. These will be ignored.
- Name consisting of a non-blank character string
- Two integers, Nr and Nc, representing the numbers of rows and columns respectively
- Nr number of data points, consisting of Nc floats separated by whitespace
For example:
...
*
* Polyline L008
*
L008
4 2
131595.0 549685.0
131750.0 549865.0
131595.0 550025.0
131415.0 550175.0
...
Note that the points can be arbitrarily indented, and the comments are optional.
if no has_z_value has been defined, it will be based on the file path extensions of the filepath: - .pliz will default to True - .pli and .pol will default to False
Empty lines and unexpected whitespace will be flagged as warnings, and ignored.
If invalid syntax is detected within a block, an error will be created. This block will be ignored for the purpose of creating PolyObject instances. Once an error is encountered, any following lines will be marked as part of the invalid block, until a new valid block is found. Note that this means that sequential invalid blocks will be reported as a single invalid block. Such invalid blocks will be reported as warnings.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
filepath |
Path |
Path to the pli(z)/pol convention structured file. |
required |
has_z_values |
bool |
Whether to create points containing a z-value. Defaults to None. |
required |
Returns:
Type | Description |
---|---|
Dict |
The dictionary describing the data of a PolyObject. |
Source code in hydrolib/core/io/polyfile/parser.py
def read_polyfile(filepath: Path, has_z_values: bool) -> Dict:
"""Read the specified file and return the corresponding data.
The file is expected to follow the .pli(z) / .pol convention. A .pli(z) or .pol
file is defined as consisting of a number of blocks of lines adhering to the
following format:
- Optional description record consisting of one or more lines starting with '*'.
These will be ignored.
- Name consisting of a non-blank character string
- Two integers, Nr and Nc, representing the numbers of rows and columns respectively
- Nr number of data points, consisting of Nc floats separated by whitespace
For example:
```
...
*
* Polyline L008
*
L008
4 2
131595.0 549685.0
131750.0 549865.0
131595.0 550025.0
131415.0 550175.0
...
```
Note that the points can be arbitrarily indented, and the comments are optional.
if no has_z_value has been defined, it will be based on the file path
extensions of the filepath:
- .pliz will default to True
- .pli and .pol will default to False
Empty lines and unexpected whitespace will be flagged as warnings, and ignored.
If invalid syntax is detected within a block, an error will be created. This block
will be ignored for the purpose of creating PolyObject instances.
Once an error is encountered, any following lines will be marked as part of the
invalid block, until a new valid block is found. Note that this means that sequential
invalid blocks will be reported as a single invalid block. Such invalid blocks will
be reported as warnings.
Args:
filepath:
Path to the pli(z)/pol convention structured file.
has_z_values:
Whether to create points containing a z-value. Defaults to None.
Returns:
Dict: The dictionary describing the data of a PolyObject.
"""
if has_z_values is None:
has_z_values = _determine_has_z_value(filepath)
parser = Parser(filepath, has_z_value=has_z_values)
with filepath.open("r") as f:
for line in f:
parser.feed_line(line)
objs = parser.finalize()
return {"has_z_values": has_z_values, "objects": objs}
Serializer¶
Serializer
¶
Serializer provides several static serialize methods for the models.
serialize_description(description: Optional[hydrolib.core.io.polyfile.models.Description]) -> Iterable[str]
staticmethod
¶
Serialize the Description to a string which can be used within a polyfile.
Returns:
Type | Description |
---|---|
str |
The serialised equivalent of this Description |
Source code in hydrolib/core/io/polyfile/serializer.py
@staticmethod
def serialize_description(description: Optional[Description]) -> Iterable[str]:
"""Serialize the Description to a string which can be used within a polyfile.
Returns:
str: The serialised equivalent of this Description
"""
if description is None:
return []
if description.content == "":
return [
"*",
]
return (f"*{v.rstrip()}" for v in description.content.splitlines())
serialize_metadata(metadata: Metadata) -> Iterable[str]
staticmethod
¶
Serialize this Metadata to a string which can be used within a polyfile.
The number of rows and number of columns are separated by four spaces.
Returns:
Type | Description |
---|---|
str |
The serialised equivalent of this Metadata |
Source code in hydrolib/core/io/polyfile/serializer.py
@staticmethod
def serialize_metadata(metadata: Metadata) -> Iterable[str]:
"""Serialize this Metadata to a string which can be used within a polyfile.
The number of rows and number of columns are separated by four spaces.
Returns:
str: The serialised equivalent of this Metadata
"""
return [metadata.name, f"{metadata.n_rows} {metadata.n_columns}"]
serialize_point(point: Point) -> str
staticmethod
¶
Serialize this Point to a string which can be used within a polyfile.
the point data is indented with 4 spaces, and the individual values are separated by 4 spaces as well.
Returns:
Type | Description |
---|---|
str |
The serialised equivalent of this Point |
Source code in hydrolib/core/io/polyfile/serializer.py
@staticmethod
def serialize_point(point: Point) -> str:
"""Serialize this Point to a string which can be used within a polyfile.
the point data is indented with 4 spaces, and the individual values are
separated by 4 spaces as well.
Returns:
str: The serialised equivalent of this Point
"""
z_val = f"{point.z} " if point.z is not None else ""
data_vals = " ".join(str(v) for v in point.data)
return f" {point.x} {point.y} {z_val}{data_vals}".rstrip()
serialize_poly_object(obj: PolyObject) -> Iterable[str]
staticmethod
¶
Serialize this PolyObject to a string which can be used within a polyfile.
Returns:
Type | Description |
---|---|
str |
The serialised equivalent of this Point |
Source code in hydrolib/core/io/polyfile/serializer.py
@staticmethod
def serialize_poly_object(obj: PolyObject) -> Iterable[str]:
"""Serialize this PolyObject to a string which can be used within a polyfile.
Returns:
str: The serialised equivalent of this Point
"""
description = Serializer.serialize_description(obj.description)
metadata = Serializer.serialize_metadata(obj.metadata)
points = map(Serializer.serialize_point, obj.points)
return chain(description, metadata, points)
write_polyfile(path: Path, data: Dict) -> None
¶
Write the data to a new file at path
Parameters:
Name | Type | Description | Default |
---|---|---|---|
path |
Path |
The path to write the data to |
required |
data |
PolyFile |
The data to write |
required |
Source code in hydrolib/core/io/polyfile/serializer.py
def write_polyfile(path: Path, data: Dict) -> None:
"""Write the data to a new file at path
Args:
path (Path): The path to write the data to
data (PolyFile): The data to write
"""
serialized_data = chain.from_iterable(
map(Serializer.serialize_poly_object, data["objects"])
)
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w") as f:
for line in serialized_data:
f.write(line)
f.write("\n")