Introduction

The caris Python API provides a consistent data model for Datasets in a variety of formats.

This tutorial demonstrates how to use the caris module. The features in this module are commonly used in any CARIS Python API script.

Dataset Objects

Datasets are collections of Features, and each Feature is defined by its Geometry and attributes.

Opening a Dataset from a file

Use the caris.open() function to open a supported dataset. Supported file types include ESRI Shapefile, GML, KML, MapInfo, SQLite, and GeoPackage.

The caris.open() function requires an argument for either a uri or a file_name path to a supported dataset.

import caris

shapefile_path = r"Data\example.shp"
shapefile = caris.open(file_name=shapefile_path)

You can use caris.open() to open a file as a Dataset in read/write mode by passing caris.OpenMode.READ_WRITE to its open_mode argument.

Changes to a Dataset are not saved until its .commit() method is called.

import caris

dataset = caris.open(file_name=r"Data\example.gpkg",
                     open_mode=caris.OpenMode.READ_WRITE)

for feature in dataset:
    feature['INFORM'] = 'Edited by Cartographer'

dataset.commit()

Extended Dataset class

The caris.Dataset class is extended by the caris.bathy.db.Dataset class, which adds database-specific attributes and methods to the base Dataset class.

For example, the attribute .current_time, and the methods .query_journal() and .query_offline(), are only available in the extended caris.bathy.db.Dataset class.

Iterating over a Dataset

Dataset objects are iterable, and the result of each iteration step is a Feature object.

import caris

shapefile_path = r"Data\example.shp"
shapefile_dataset = caris.open(file_name=shapefile_path)

for feature in shapefile_dataset:
    print(feature)
<caris._py_caris.Feature object at 0x0000024A8C999190>
<caris._py_caris.Feature object at 0x0000024A8C9BB8C8>
<caris._py_caris.Feature object at 0x0000024A8C999190>

Querying a Dataset

The Dataset class contains methods for querying its Features. The results of a query are returned as FeatureRange objects, which can be iterated over to get the results as Feature objects.

Query all Features

Use the .query_all() method to retrieve all features in a Dataset.

import caris

dataset = caris.open(file_name=r"Data\example.gpkg")

all_features = dataset.query_all()
print(all_features)

for feature in all_features:
    print(feature)
<caris._py_caris.FeatureRange object at 0x000001F18F9775A0>
<caris._py_caris.Feature object at 0x000001F18F958190>
<caris._py_caris.Feature object at 0x000001F18F97B8C8>
<caris._py_caris.Feature object at 0x000001F18F958190>

Since FeatureRange objects are iterators, you can convert them to a list using the built-in list() function.

import caris

dataset = caris.open(file_name=r"Data\example.gpkg")

all_features = list(dataset.query_all())
print(all_features)
[<caris._py_caris.Feature object at 0x00000284BA914190>, <caris._py_caris.Feature object at 0x00000284BA93C8C8>, <caris._py_caris.Feature object at 0x00000284BA93C920>, <caris._py_caris.Feature object at 0x00000284BA93C978>, <caris._py_caris.Feature object at 0x00000284BA93C9D0>]

Query by feature type

You can restrict your query to a single feature type using the .query() method and passing the desired feature code as the first argument.

import caris

dataset = caris.open(file_name=r"Data\example.gpkg")

surfac_features = dataset.query('surfac')

for surfac_feature in surfac_features:
    print(surfac_feature['OBJNAM'])
Area6-7125 400kHz-Fresh Water Seeps_1m
EM2040_NIWA_Sediment_types_05m
EM2040_NIWA_Seadefence_structure_05m

Query using CQL

The .query() method accepts the keyword argument CQL into which you can pass a string written in Common Query Language by the Open Geospatial Consortium.

import caris

dataset = caris.open(file_name=r"Data\example.gpkg")

cql = "SUREND > 20110301"

surfac_features = dataset.query('surfac', CQL=cql)

for feature in surfac_features:
    print(feature['SUREND'])
20110427
20110309

CQL queries support functions such as IS, CONTAINS, and LIKE. You can use the Advanced tab of the Add Layer by Filter dialog box in BASE Editor for help with CQL syntax.

import caris

dataset = caris.open(file_name=r"Data\example.gpkg")

cql = "OBJNAM LIKE '%NIWA%' AND TECSOU IS { 'found by multi-beam' }"

surfac_features = dataset.query('surfac', CQL=cql)

for feature in surfac_features:
    print(feature['OBJNAM'])
EM2040_NIWA_Sediment_types_05m
EM2040_NIWA_Seadefence_structure_05m

For more information about CQL queries, visit:

https://docs.geoserver.org/stable/en/user/tutorials/cql/cql_tutorial.html


Dataset Catalogues

A catalogue sets up features, attributes, and relations that are part of a Dataset.

The catalogue contains information about a Dataset such as:

  • Feature types that belong to the Dataset.
  • Attributes that belong to each Feature.
  • Attributes that are mandatory.
  • Permitted attribute values.
  • Master-slave relations that may exist between features.

FeatureCatalogue Objects

FeatureCatalogue objects provide information about which FeatureDefinitions belong to the catalogue.

You can get the FeatureCatalogue of a Dataset using its .catalogue attribute.

import caris

dataset_path = r"Data\example.gpkg"
dataset = caris.open(file_name=dataset_path)

print(dataset.catalogue)
<caris._py_caris.FeatureCatalogue object at 0x000002045A8F1190>

The FeatureCatalogue.definitions attribute is a list of FeatureDefinitions in the catalogue.

import caris

dataset_path = r"Data\example.gpkg"
dataset = caris.open(file_name=dataset_path)
catalogue = dataset.catalogue

print(catalogue.definitions)
[FeatureDefinition('surfac'), FeatureDefinition('survey')]

You can retrieve a particular FeatureDefinition by passing its feature code to the FeatureCatalogue.get_definition() method.

import caris

dataset_path = r"Data\example.gpkg"
dataset = caris.open(file_name=dataset_path)
catalogue = dataset.catalogue

print(catalogue.get_definition('surfac'))
FeatureDefinition('surfac')

FeatureDefinition Objects

FeatureDefinition objects provide information about feature type definitions in a FeatureCatalogue.

import caris

dataset_path = r"Data\example.gpkg"
dataset = caris.open(file_name=dataset_path)

surfac_definition = dataset.catalogue.get_definition('surfac')

print(surfac_definition.code)
print(surfac_definition.primitiveTypes)
surfac
[caris._py_caris.PrimitiveType.AREA_2D, caris._py_caris.PrimitiveType.LINE_2D, caris._py_caris.PrimitiveType.POINT_2D]

You can get the AttributeDefinitionDictionary object representing attribute definitions belonging to the FeatureDefinition from its .attributes attribute.

import caris

dataset_path = r"Data\example.gpkg"
dataset = caris.open(file_name=dataset_path)

surfac_definition = dataset.catalogue.get_definition('surfac')

print(type(surfac_definition.attributes))
print(surfac_definition.attributes)
<class 'caris._py_caris.AttributeDefinitionDictionary'>
[AttachedFiles,CATZOC,CPDATE,CoverageCRS,CoverageDescriptor,CoverageId,CoverageName,CoverageResolution,CoverageSource,CoverageType,CreatingUser,CreationTime,DRVAL1,DRVAL2,DUNITS,FeatureId,HORDAT,HUNITS,INFORM,MasterId,ModificationTime,NINFOM,OBJNAM,POSACC,RECDAT,RECIND,SORDAT,SORIND,SOUACC,STATUS,SUREND,SURSTA,SURTYP,TECSOU,VERDAT,planam,srfcat]

AttributeDefinitionDictionary Objects

AttributeDefinitionDictionary objects provide information about what attributes belong to a FeatureDefinition.

AttributeDefinitionDictionary are dictionary-like objects where the key is an attribute code and the value is an AttributeDefinition.

import caris

dataset_path = r"Data\example.gpkg"
dataset = caris.open(file_name=dataset_path)

surfac_definition = dataset.catalogue.get_definition('surfac')
attribute_definition_dictionary = surfac_definition.attributes

print(type(attribute_definition_dictionary["OBJNAM"]))
print(attribute_definition_dictionary["OBJNAM"])
<class 'caris._py_caris.AttributeDefinition'>
<Code>OBJNAM</Code>
<Name>OBJNAM</Name>
<Description></Description>
<Type>
    <Kind>String</Kind>
    <Code>OBJNAM</Code>
    <Name>OBJNAM</Name>
    <Description></Description>
    <MinLength></MinLength>
    <MaxLength></MaxLength>
    <Default></Default>
    <Control></Control>
    <Format>
    </Format>
</Type>
<ReadOnly>0</ReadOnly>
<Hidden>0</Hidden>
<Conditional>0</Conditional>
<UnknownPermitted>0</UnknownPermitted>
<MinOccurs>0</MinOccurs>
<MaxOccurs> 1</MaxOccurs>

AttributeDefinitionDictionary have many of the same features as standard dictionary objects. For example, you can get a view of its key/value pairs using the .items() method.

import caris

dataset_path = r"Data\example.gpkg"
dataset = caris.open(file_name=dataset_path)

surfac_definition = dataset.catalogue.get_definition('surfac')
attribute_definition_dictionary = surfac_definition.attributes

for code, attribute_definition in attribute_definition_dictionary.items():
    print("key: {:<20}value: {}".format(code, attribute_definition))
key: AttachedFiles       value: AttributeDefinition('AttachedFiles')
key: CATZOC              value: AttributeDefinition('CATZOC')
key: CPDATE              value: AttributeDefinition('CPDATE')
# ...
# OUTPUT PARTIALLY HIDDEN
# ...
key: VERDAT              value: AttributeDefinition('VERDAT')
key: planam              value: AttributeDefinition('planam')
key: srfcat              value: AttributeDefinition('srfcat')

Feature Objects

Use Feature objects to work with features. Features are defined by their geometry, attribute definition, and attribute values.

Creating a Feature in a Dataset

You can create a new Feature in an existing Dataset by calling the Dataset.create_feature() method. Creating a Feature requires a Geometry object to specify its geometry.

import caris

# Create CoordinateReferenceSystem for Geometry
wgs84_crs = caris.CoordinateReferenceSystem('EPSG', '4326')

# Create Geometry of new feature representing CARIS office building footprint
office_wkt = """
POLYGON (( -66.6627 45.9590, -66.6634 45.9592, 
           -66.6632 45.9594, -66.6629 45.9593, 
           -66.6628 45.9595, -66.6625 45.9593, 
           -66.6627 45.9590 ))
"""
office_geom = caris.Geometry(wgs84_crs, office_wkt)

# Open Fredericton building footprints dataset
footprint_dataset = caris.open(file_name=r'Data\BuildingFootprint.shp',
                               open_mode=caris.OpenMode.READ_WRITE)

# Create CARIS office footprint feature
office_feature = footprint_dataset.create_feature('BuildingFootprint', office_geom)

# Set attributes
office_feature['KEYWORD'] = 'CARIS HQ'

# Save changes to dataset
footprint_dataset.commit()

Extended Feature Class

The caris.Feature class is extended by the caris.bathy.db.Feature class, which adds database-specific methods and attributes to the base Feature class.

For example, the attributes .attachments and .id, and the methods .download_attachment() and .purge() are only available in the extended caris.bathy.db.Feature class.

Features queried from the base caris.Dataset class will belong to the base caris.Feature class, while features queried from the extended caris.bathy.db.Dataset class will belong to the extended caris.bathy.db.Feature class.


AttributeDictionary Objects

You can work with Feature attributes using caris.AttributeDictionary objects. Every Feature has an caris.AttributeDictionary in its .attributes attribute.

Accessing Feature Attributes

You can index Feature objects in a manner similar to a dictionary. Enter an attribute code between square brackets to access its value.

import caris

dataset_path = r"Data\example.gpkg"
dataset = caris.open(file_name=dataset_path)

# Get first surfac feature in dataset
feature = list(dataset.query('surfac'))[0]
print(feature['OBJNAM'])
Area6-7125 400kHz-Fresh Water Seeps_1m

Only attributes that have been set can be indexed. In the following example, both “OBJNAM” and “INFORM” are valid attributes, but only “OBJNAM” has been set. Attempting to index the “INFORM” attribute throws a KeyError.

import caris

dataset_path = r"Data\example.gpkg"
dataset = caris.open(file_name=dataset_path)

# Get first surfac feature in dataset
feature = list(dataset.query('surfac'))[0]

print(feature['OBJNAM']) # Attribute has been set
print(feature['INFORM']) # Attribute has not been set
Area6-7125 400kHz-Fresh Water Seeps_1m
Traceback (most recent call last):
File "c:\Work\CPP\Projects\framework_5\python\py_caris\documentation\caris\samples\access_unset_feature_attribute.py", line 10, in <module>
    print(feature['INFORM']) # Attribute has not been set
KeyError: 'INFORM'

caris.AttributeDictionary objects have a .get() method, which you can use as an alternative way to access an attribute value. Rather than throw an error, the .get() method returns None if an attribute has not been set. Optionally, a default value may be returned instead.

import caris

dataset_path = r"Data\example.gpkg"
dataset = caris.open(file_name=dataset_path)

# Get first surfac feature in dataset
feature = list(dataset.query('surfac'))[0]

print(feature.attributes.get('OBJNAM')) # Attribute has been set
print(feature.attributes.get('INFORM')) # Attribute has not been set
print(feature.attributes.get('INFORM', '<undefined>')) # Using optional default value
Area6-7125 400kHz-Fresh Water Seeps_1m
None
<undefined>

Setting Feature Attributes

You can set Feature object attributes by assigning a value to its AttributeDictionary. The changes are applied when the .commit() method is called on the Dataset.

The type of data accepted by an AttributeDictionary depends on the attribute’s type in the Dataset catalogue.

Note

The following examples use a Bathy DataBASE Dataset so that all attribute types are represented. For more information about working with the caris.bathy.db module, see BDB Server Module.

import caris.bathy.db as bdb

NM_USER, NM_PASS, NM_HOST, DB_NAME = ('dba', 'sql', 'localhost', 'Training')
node_manager = bdb.NodeManager(NM_USER, NM_PASS, NM_HOST)
dataset = node_manager.get_database(DB_NAME)

# Get first surfac and survey features in dataset
surfac = list(dataset.query('surfac'))[0]
survey = list(dataset.query('survey'))[0]

# Set STRING type attributes from str
surfac["OBJNAM"] = "New Name"

# Set ALPHANUMERIC type attributes from str
surfac["RECIND"] = "CA,1C,digi"

# Set FLOAT type attributes from str or number
surfac["SOUACC"] = "3.7"
surfac["POSACC"] = 1.1

# Set INTEGER type attributes from str or number
# Float inputs are truncated
survey["CSCALE"] = 10000
survey["SDISMX"] = "5"

# Set DATE type attributes from str
# Format is not validated, but should be YYYYMMDD
survey["RECDAT"] = "19890921"

# Set ENUMERATION type attributes from int or str
# Both value and description are accepted
surfac["CATZOC"] = 1
surfac["DUNITS"] = "3"
surfac["VERDAT"] = "Low Water"

# Set LIST type attributes from list of int or str
# Both value and description are accepted
surfac["TECSOU"] = [1, "3", "found by laser"]

# # list methods like .append() work on LIST type attributes
surfac["TECSOU"].append(5)

print("Saving dataset changes.")
dataset.commit()

You can set an attribute to undefined by assigning it to a value that depends on the attribute’s type.

import caris.bathy.db as bdb

NM_USER, NM_PASS, NM_HOST, DB_NAME = ('dba', 'sql', 'localhost', 'Training')
node_manager = bdb.NodeManager(NM_USER, NM_PASS, NM_HOST)
dataset = node_manager.get_database(DB_NAME)

# Get first surfac and survey features in dataset
surfac = list(dataset.query('surfac'))[0]
survey = list(dataset.query('survey'))[0]

# Unset STRING type attributes with empty str
surfac["OBJNAM"] = ""

# Unset ALPHANUMERIC type attributes with empty str or list, or None
surfac["RECIND"] = ""
surfac["SORIND"] = []
surfac["CPDATE"] = None

# Unset FLOAT type attributes from None or empty list
surfac["SOUACC"] = None
surfac["POSACC"] = []

# Unset INTEGER type attributes from None or empty list
survey["CSCALE"] = None
survey["SDISMX"] = []

# Unset DATE type attributes with empty str or list, or None
survey["RECDAT"] = ""
surfac["RECDAT"] = []
surfac["RECDAT"] = None

# Unset ENUMERATION type attributes with empty str or list
surfac["DUNITS"] = ""
surfac["CATZOC"] = []

# Unset LIST type attributes with empty str or list
surfac["TECSOU"] = ""
surfac["STATUS"] = []

print("Saving dataset changes.")
dataset.commit()

Validating Feature Attributes

Validate Feature attributes before saving them using the AttributeDictionary.validate() method. The .validate() method throws a RuntimeError if there are invalid attributes in the AttributeDictionary. Use it within a try block.

import caris.bathy.db as bdb

NM_USER, NM_PASS, NM_HOST, DB_NAME = ('dba', 'sql', 'localhost', 'Training')
node_manager = bdb.NodeManager(NM_USER, NM_PASS, NM_HOST)
dataset = node_manager.get_database(DB_NAME)

# Get first surfac feature in dataset
surfac = list(dataset.query('surfac'))[0]

# Setting ENUMERATION type attribute to invalid value
surfac["VERDAT"] = "Loww Water"

try:
    surfac.attributes.validate()
except RuntimeError as e:
    print("ERROR: Attributes are invalid, rolling back changes.")
    dataset.rollback()
else:
    print("Saving dataset changes.")
    dataset.commit()
ERROR: Attributes are invalid, rolling back changes.

Storing Attributes as XML

The AttributeDictionary.to_xml() method returns an xml formatted string. You can use this string later to set attributes using the AttributeDictionary.from_xml() method.

In the following example, a Feature's attributes are dumped into an xml formatted string and saved to a file.

import caris
import os

dataset_path = r"Data\example.gpkg"
dataset = caris.open(file_name=dataset_path)

# Create CQL query for particular FeatureId
feature_id = "f49da13c-e5d9-4bd6-a525-5289f95b4759"
cql = "FeatureId = '{}'".format(feature_id)

# Get the feature
feature = list(dataset.query('surfac', CQL=cql))[0]

# Read XML string from AttributeDictionary
attributes_xml = feature.attributes.to_xml()

# Build path to attribute xml dump
attribute_xml_path = os.path.join("Data", feature_id) + ".xml"

with open(attribute_xml_path, mode="w") as attribute_xml_file:
    attribute_xml_file.write(attributes_xml)

You can later load the attributes xml file to set the attributes of a different Feature.

import caris

dataset_path = r"Data\example.gpkg"
dataset = caris.open(file_name=dataset_path,
                     open_mode=caris.OpenMode.READ_WRITE)

# Create a new Feature in Dataset
new_polygon = """
POLYGON (( 174.8226113309640700 -41.2852411386752750,
174.8148263698648200 -41.3014979692060250,
174.8331439253924200 -41.3088249914170620,
174.8226113309640700 -41.2852411386752750 ))
"""
new_geometry = caris.Geometry(dataset.crs, new_polygon)
new_feature = dataset.create_feature('surfac', new_geometry)

# Load attributes xml to string
attributes_xml_path = r"Data\f49da13c-e5d9-4bd6-a525-5289f95b4759.xml"
with open(attributes_xml_path) as attributes_xml_file:
    attributes_xml = attributes_xml_file.read()

# Apply loaded attributes to new Feature
new_feature.attributes.from_xml(attributes_xml)

print("Saving dataset changes.")
dataset.commit()

CoordinateReferenceSystem Objects

CoordinateReferenceSystem objects represent a known coordinate reference system. Coordinate reference systems are used to correctly place geometry in the real world.

Every Dataset object has an associated CoordinateReferenceSystem stored in its .crs attribute.

CoordinateReferenceSystem Object Constructor

You can create a CoordinateReferenceSystem object from a Well Know Text string, or from an EPSG code.

To create one from Well Known Text, the first argument to the constructor must be ‘WKT’.

import caris

wgs84_wkt = """
    GEOGCS["WGS 84",
        DATUM["World Geodetic System 1984",
            SPHEROID["WGS 84",6378137,298.2572235629972,
                AUTHORITY["EPSG","7030"]],
            AUTHORITY["EPSG","6326"]],
        PRIMEM["Greenwich",0,
            AUTHORITY["EPSG","8901"]],
        UNIT["degree (supplier to define representation)",0.0174532925199433,
            AUTHORITY["EPSG","9122"]],
        AUTHORITY["EPSG","4326"]]
"""
wgs84_crs = caris.CoordinateReferenceSystem('WKT', wgs84_wkt)

To create one from an EPSG code, the first argument to the constructor must be ‘EPSG’.

import caris

wgs84_crs = caris.CoordinateReferenceSystem('EPSG', '4326')

Geometry Objects

Geometry objects represent geometric information about points, lines, and areas in a known coordinate reference system.

Every Feature object has an associated Geometry stored in its .geometry attribute.

Geometry Object Constructor

You can create a new Geometry object from a Well Known Text string and a CoordinateReferenceSystem object.

import caris

# Create CoordinateReferenceSystem for Geometry
wgs84_crs = caris.CoordinateReferenceSystem('EPSG', '4326')

# Create Geometry of new feature representing CARIS office building footprint
office_wkt = """
POLYGON (( -66.6627 45.9590, -66.6634 45.9592, 
           -66.6632 45.9594, -66.6629 45.9593, 
           -66.6628 45.9595, -66.6625 45.9593, 
           -66.6627 45.9590 ))
"""
office_geom = caris.Geometry(wgs84_crs, office_wkt)

Geometry queries

You can use Geometry objects to query a Dataset. The Dataset.query() method has a series of keyword arguments that accept Geometry objects. Each of these filters the Features returned by the query a different way.

For most common cases, the intersects query is appropriate.

import caris

# Create CoordinateReferenceSystem for Geometry
wgs84_crs = caris.CoordinateReferenceSystem('EPSG', '4326')

# Create Geometry of new feature representing CARIS office building footprint
office_wkt = """
POLYGON (( -66.6627 45.9590, -66.6634 45.9592, 
           -66.6632 45.9594, -66.6629 45.9593, 
           -66.6628 45.9595, -66.6625 45.9593, 
           -66.6627 45.9590 ))
"""
office_geom = caris.Geometry(wgs84_crs, office_wkt)

# Open Fredericton building footprints dataset
footprint_dataset = caris.open(file_name=r"Data\BuildingFootprint.shp")

# Query for intersecting features
query_results = footprint_dataset.query("BuildingFootprint", intersects=office_geom)

for feature in query_results:
    print(feature["OBJECTID"])
164

Geometry Transformations

You can transform a Geometry object into a different coordinate reference system using its .transform() method.

import caris

# Create CoordinateReferenceSystem for Geometry
wgs84_crs = caris.CoordinateReferenceSystem('EPSG', '4326')

# Create Geometry of new feature representing CARIS office building footprint
office_wkt = """
POLYGON (( -66.6627 45.9590, -66.6634 45.9592, 
           -66.6632 45.9594, -66.6629 45.9593, 
           -66.6628 45.9595, -66.6625 45.9593, 
           -66.6627 45.9590 ))
"""
office_geom = caris.Geometry(wgs84_crs, office_wkt)

# Create NAD83-UTM19 CoordinateReferenceSystem for transformed Geometry
nad83_utm19_crs = caris.CoordinateReferenceSystem('EPSG', '26919')
transformed_office_geom = office_geom.transform(nad83_utm19_crs)

print(transformed_office_geom.wkt)
POLYGON (( 681114.5603214782900000 5092148.3724222146000000, 681059.6673699112100000 5092169.0018110983000000, 681074.5129666726600000 5092191.6770132380000000, 681098.0849759923300000 5092181.2486592364000000, 681105.1817335451500000 5092203.6966117388000000, 681129.0797691545400000 5092182.1580670280000000, 681114.5603214782900000 5092148.3724222146000000 ))