"""
Summary:
Contains the AUnit, CommentUnit, HeaderUnit and UnknownSection
classes.
The AUnit is an abstract base class for all types of Isis unit read
in through the ISIS data file. All section types that are built should
inherit from the AUnit baseclass.
Author:
Duncan Runnacles
Created:
01 Apr 2016
Copyright:
Duncan Runnacles 2016
TODO:
Updates:
"""
from __future__ import unicode_literals
import hashlib
import uuid
import random
import copy
# from abc import ABCMeta, abstractmethod
from ship.fmp.datunits import ROW_DATA_TYPES as rdt
from ship.datastructures import DATA_TYPES as dt
from ship.fmp.headdata import HeadDataItem
import logging
logger = logging.getLogger(__name__)
"""logging references with a __name__ set to this module."""
[docs]class AUnit(object):
"""Abstract base class for all Dat file units.
This class must be inherited by all classes representing an isis
data file unit (such as River, Junction, Culvert, etc).
Every subclass should override the readUnitData() and getData() methods to
ensure they are specific to the setup of the individual units variables.
If they are not overridden this class will simply take the data and store it
as read and provide it back in the same state.
All calls from the client to these classes should create the object and then
call the readUnitData() method with the raw data.
There is an UknownSection class at the bottom of this file that can be used
for all parts of the isis dat file that have not had a class defined. It just
calls the basic read-in read-out methods from this class and understands nothing
about the structure of the file section it is holding.
If you are creating subclass of this that has row_data (see below) you
should make sure that you call the setDummyRow() method in each of the
RowDataCollections. Otherwise, if the user doesn't add any rows, FMP will
throw errors.
See Also:
UnknownSection
"""
# __metaclass__ = ABCMeta
def __init__(self, **kwargs):
"""Constructor
Set the defaults for all unit specific variables.
These should be set by each unit at some point in the setup process.
E.g. RiverUnit would set type and UNIT_CATEGORY at __init__() while name
and data_objects are set in the readUnitData() method.
Both of these are called at or immediately after initialisation.
"""
self._name = kwargs.get('name', 'unknown') # Unit label
self._name_ds = kwargs.get('name_ds', 'unknown') # Unit downstream label
self._data = None
"""This is used for catch-all data storage.
Used in units such as UnknownSection.
Classes that override the readUnitData() and getData() methods are
likely to ignore this variable and use row_collection and head_data instead.
"""
self._unit_type = 'Unknown'
"""The type of ISIS unit - e.g. 'River'"""
self._unit_category = 'Unknown'
"""The ISIS unit category - e.g. for type 'Usbpr' it would be 'Bridge'"""
self.row_data = {}
"""Collection containing all of the ADataRow objects.
This is the main collection for row data in any unit that contains it.
In a RiverUnit, for example, this will hold the RowDataObject's
containing the CHAINAGE, ELEVATION, etc.
"""
self.head_data = {}
"""Dictionary containing set values that are always present in the file.
In a RiverUnit this includes values like slope and distance. I.e.
values that appear in set locations, usually at the top of the unit
data in the .dat file.
"""
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
@property
def name_ds(self):
return self._name_ds
@name_ds.setter
def name_ds(self, value):
self._name_ds = value
@property
def has_ics(self):
if not self.icLabels():
return False
else:
return True
@property
def has_row_data(self):
if not self.row_data:
return False
else:
return True
@property
def unit_type(self):
return self._unit_type
@property
def unit_category(self):
return self._unit_category
[docs] def icLabels(self):
"""Returns the initial_conditions values for this object.
This method should be overriden by all classes that contain intial
conditions.
For example a BridgeUnit type will have two initial conditions labels;
the upstream and downstream label names.
By default this will return an empty list.
Return:
list - of intial condition label names.
"""
return []
[docs] def linkLabels(self):
"""Dict of all the names that the unit references.
For a RiverUnit this is only the self.name + spills and laterals,
for a bridge it would be self.name, self.name_ds, self.remote_us,
self.remote_ds and for a JunctionUnit it could be many more.
It can be used to identify which other units are directly associated to
this one in some way.
Return:
dict - containing all referenced names.
"""
return {'name': self._name}
[docs] def copy(self):
"""Returns a copy of this unit with it's own memory allocation."""
object_copy = copy.deepcopy(self)
return object_copy
[docs] def rowDataObject(self, key, rowdata_key='main'):
"""Returns the row data object as a list.
This will return the row_collection data object referenced by the key
provided in list form.
If you intend to update the values you should use getRowDataObject
instead as the data provided will be mutable and therefore reflected in
the values held by the row_collection. If you just want a quick way to
loop through the values in one of the data objects and only intend to
read the data then use this.
Args:
key (int): the key for the data object requested. It is best to use
the class constants (i.e. RiverUnit.CHAINAGE) for this.
rowdata_key(str): key to a RowDataCollection in row_data.
Returns:
List containing the data in the DataObject that the key points
to. Returns false if there is no row collection.
Raises:
KeyError: If key or rowdata_key don't exist.
"""
if not self.has_row_data: return None
return self.row_data[rowdata_key].dataObject(key)
[docs] def row(self, index, rowdata_key='main'):
"""Get the data vals in a particular row by index.
Args:
index(int): the index of the row to return.
Return:
dict - containing the values for the requested row.
"""
if not self.has_row_data: return None
return self.row_data[rowdata_key].rowAsDict(index)
[docs] def getData(self):
"""Getter for the unit data.
Return the file geometry data formatted ready for saving in the style
of an ISIS .dat file
Note:
This method should be overriden by the sub class to restore the
data to the format required by the dat file.
Returns:
List of strings - formatted for writing to .dat file.
"""
raise NotImplementedError
[docs] def readUnitData(self, data, file_line, **kwargs):
"""Reads the unit data supplied to the object.
This method is called by the FmpUnitFactory class when constructing the
Isis unit based on the data passed in from the dat file.
The default hook just copies all the data parsed in the buildUnit()
method of the factory and aves it to the given unit. This is exactly
what happens for the UnknownUnit class that just maintains a copy of the
unit data exactly as it was read in.
Args:
data (list): raw data for the section as supplied to the class.
Note:
When a class inherits from AUnit it should override this method
with unit specific load behaviour. This is likely to include:
populate unit specific header value dictionary and in some units
creating row data object.
See Also:
RiverSection for an example of overriding this method with a
concrete class.
"""
self.head_data['all'] = data
[docs] def deleteRow(self, index, rowdata_key='main', **kwargs):
"""Removes a data row from the RowDataCollection.
**kwargs:
These are passed onto the RowDataCollection. See there for details.
"""
if index < 0 or index >= self.row_data[rowdata_key].numberOfRows():
raise IndexError ('Given index is outside bounds of row_data[rowdata_key] data')
self.row_data[rowdata_key].deleteRow(index, **kwargs)
[docs] def updateRow(self, row_vals, index, rowdata_key='main', **kwargs):
"""Update an existing data row in one of the RowDataCollection's.
**kwargs:
These are passed onto the RowDataCollection. See there for details.
Args:
row_vals(dict): Named arguments required for adding a row to the
collection. These will be as stipulated by the way that a
concrete implementation of this class setup the collection.
rowdata_key='main'(str): the name of the RowDataCollection
held by this to add the new row to. If None it is the
self.row_collection. Otherwise it is the name of one of the
entries in the self.additional_row_collections dictionary.
index=None(int): the index in the RowDataObjectCollection to update
with the row_vals
"""
if index >= self.row_data[rowdata_key].numberOfRows():
raise IndexError ('Given index is outside bounds of row_collection data')
# Call the row collection add row method to add the new row.
self.row_data[rowdata_key].updateRow(row_vals=row_vals, index=index, **kwargs)
[docs] def addRow(self, row_vals, rowdata_key='main', index=None, **kwargs):
"""Add a new data row to one of the row data collections.
Provides the basics of a function for adding additional row dat to one
of the RowDataCollection's held by an AUnit type.
Checks that key required variables: ROW_DATA_TYPES.CHAINAGE amd
ROW_DATA_TYPES.ELEVATION are in the kwargs and that inserting chainge in
the specified location is not negative, unless check_negatie == False.
It then passes the kwargs directly to the RowDataCollection's
addNewRow function. It is the concrete class implementations
respnsobility to ensure that these are the expected values for it's
row collection and to set any defaults. If they are not as expected by
the RowDataObjectCollection a ValueError will be raised.
**kwargs:
These are passed onto the RowDataCollection. See there for details.
Args:
row_vals(dict): Named arguments required for adding a row to the
collection. These will be as stipulated by the way that a
concrete implementation of this class setup the collection.
rowdata_key='main'(str): the name of the RowDataCollection
held by this to add the new row to. If None it is the
self.row_collection. Otherwise it is the name of one of the
entries in the self.additional_row_collections dictionary.
index=None(int): the index in the RowDataObjectCollection to insert
the row into. If None it will be appended to the end.
"""
# If index is >= record length it gets set to None and is appended
if index is not None and index >= self.row_data[rowdata_key].numberOfRows():
index = None
if index is None:
index = self.row_data[rowdata_key].numberOfRows()
self.row_data[rowdata_key].addRow(row_vals, index, **kwargs)
[docs] def checkIncreases(self, data_obj, value, index):
"""Checks that: prev_value < value < next_value.
If the given value is not greater than the previous value and less
than the next value it will return False.
If an index greater than the number of rows in the row_data it will
check that it's greater than previous value and return True if it is.
Note:
the ARowDataObject class accepts a callback function called
update_callback which is called whenever an item is added or
updated. That is how this method is generally used.
Args:
data_obj(RowDataObject): containing the values to check against.
value(float | int): the value to check.
index=None(int): index to check ajacent values against. If None
it will assume the index is the last on in the list.
Returns:
False if not prev_value < value < next_value. Otherwise True.
"""
details = self._getAdjacentDataObjDetails(data_obj, value, index)
if details['prev_value']:
if not value >= details['prev_value']:
raise ValueError('CHAINAGE must be > prev index and < next index.')
if details['next_value']:
if not value <= details['next_value']:
raise ValueError('CHAINAGE must be > prev index and < next index.')
def _getAdjacentDataObjDetails(self, data_obj, value, index):
"""Safely check the status of adjacent values in an ADataRowObject.
Fetches values for previous and next indexes in the data_obj if they
exist.
Note value in return 'index' key will be the given index unless it was
None, in which case it will be the maximum index.
All other values will be set to None if they do not exist.
Args:
data_obj(RowDataObject): containing the values to check against.
value(float | int): the value to check.
index=None(int): index to check ajacent values against. If None
it will assume the index is the last on in the list.
Return:
dict - containing previous and next values and indexes, as well as
the given index checked for None.
"""
prev_value = None
next_value = None
prev_index = None
next_index = None
if index is None:
index = data_obj._max
if index < 0:
raise ValueError('Index must be > 0')
if index > 0:
prev_index = index - 1
prev_value = data_obj[prev_index]
if index < data_obj._max:
next_index = index + 1
next_value = data_obj[next_index]
retvals = {'index': index,
'prev_value': prev_value, 'prev_index': prev_index,
'next_value': next_value, 'next_index': next_index}
return retvals
[docs]class UnknownUnit(AUnit):
""" Catch all section for unknown parts of the .dat file.
This can be used for all sections of the isis dat file that have not had
a unit class constructed.
It has no knowledge of the file section that it contains and will store it
without altering it's state and return it in exactly the same format that it
received it.
This class is designed to be a fall-back class for any parts of the dat file
for which it is deemed unnecessary to deal with more carefully.
It has a 'Section' suffix rather that 'Unit' which is the naming convention
for the other unit objects because it is not necessarily a single unit. It
could be many different units.
It is created whenever the DatLoader finds
parts of the dat file that it doesn't Know how to load (i.e. there is no
*Unit defined for it. It will then put all the dat file data in one of these
until it reaches a part of the file that it does recognise.
"""
FILE_KEY = 'UNKNOWN'
FILE_KEY2 = None
def __init__ (self, **kwargs):
"""Constructor.
"""
AUnit.__init__(self, **kwargs)
self._unit_type = 'unknown'
self._unit_category = 'unknown'
self._name = 'unknown_' + str(hashlib.md5(str(random.randint(-500, 500)).encode()).hexdigest()) # str(uuid.uuid4())
[docs] def getData(self):
return self.head_data['all']
[docs] def readUnitData(self, data):
self.head_data['all'] = data