Skip to content

Utils

Module for internal utility functions to share between modules

CLASS DESCRIPTION
PolylineEditor

Allow simple polyline editing using indexes and slices

Vector

Simple Vector implementation that takes a start and end point (uses Spherical notation for theta and phi).

FUNCTION DESCRIPTION
box_on_point

Build a rectangular box on a point

center_circle

Create a circle using a center point and a radius

convert_schema

Convert a Schema from one format to another

export_project_lyrx

Pull all layers from a project file and output them in a directory as lyrx files

export_project_maps

Pull all layers from a project file and output them in a directory as mapx files

get_subtype_count

Get the subtype counts for a Table or FeatureClass

get_subtype_counts

Get a mapping of subtype counts for all featureclasses that have subtypes in the provided Dataset

iter_parts

Get a part iterator for a Polyline

iter_points

Get a point iterator for a Polyline

nat

Natural sort key for use in string sorting

patch_schema_rules

Patch an exported Schema doc by re-linking attribute rules to table names

print

Print a message to the ArcGIS Pro message queue and stdout

shortest_path

Find the shortest path or paths given a source point, target point and network of Polylines

split_at_points

Split lines at provided points

split_lines_at_points

Split a Polyline or Sequence/Iterable of polylines at provided points

two_point_circle

Create a circle using a center point and an end point

vector_at

Get the vector of the line at the given point (p-delta -> p+delta)

vectors_at

Get the vector of the line at the given point (p-delta -> p+delta)

PolylineEditor

PolylineEditor(
    polyline: Polyline, ref: SpatialReference | None = None
)

Allow simple polyline editing using indexes and slices

multipart polylines will be indexed on global point index,

    [[p1: 3pts], [p2: 2pts]] ->  [0, 1, 2, 3, 4]
                                  ^^p1^^^|^p2^^

METHOD DESCRIPTION
add_point

Insert a point in the polyline determining the ideal index based on measure

add_points

Insert points into the line based on measure

align

Adjust points of the polyline to be coincident with the target line of they are within tolerance units.

append

Append a point to the last part of the Polyline

append_part

Add a part to the polyline

copy

Return a copy of the current editor with the active polyline state as the original polyline

count

Get the count of points in the line that match the input point

dedupe

Remove all duplicate points in a line keeping last instance (returns the number of points removed)

discard

Silently remove points without raising an error if it doesn't exist

extend

Extend the last part of the polyline with the points

generalize

Generalize the polyline

index

Get the global index of the first instance of the specified point, raise a ValueError if the point is not in the Polyline

insert

Insert a point at the specified index

intersections

Iterable of Point Intersections between this line and the other

merge_lines

Merge a sequence of Polylines into one Polyline (uses union so each line becomes a part)

move

Move the polyline along a Vector

offset

Offset the line in the provided direction

orienent_with

Alter the direction of the polyline to match the direction of the input line (only works for lines that overlap)

pop

Remove a point from a polyline at the specified index (default: -1)

pop_part

pop a part from the polyline (default: -1)

project_as

Project the polyline in the given reference

remove

Remove the fist occurrence of the point from the polyline

reset

Revert all changes to polyline and restore the original geometry

reverse

Reverse the polyline parts and the points of each part of the polyline

split_at_angle

Split the polyline at points where the instantaneous angle (radians) is greater than the specified angle (radians)

split_at_point

Split the Polyline at a point returning two Polylines

split_at_points

Split the line at the provided points

ATTRIBUTE DESCRIPTION
centroid

Centroid of the feature if it is within the feature (same as measure@50%)

TYPE: PointGeometry

first_point

PointGeometry of the Polyline's firstPoint

TYPE: PointGeometry

last_point

PointGeometry of the Polyline's lastPoint

TYPE: PointGeometry

original

The original Polyline given to the PolylineEditor

TYPE: Polyline

part_editors

Get PolylineEditors for each part in the line

TYPE: list[PolylineEditor]

parts

A list of parts of the Polyline as Polylines (singlepart polylines will contain one part)

TYPE: list[Polyline]

segments

Get all 2-point segments that make up the line

TYPE: list[Polyline]

true_centroid

Center of gravity of the feature (not always in the line)

TYPE: PointGeometry

Source code in src/arcpie/utils.py
1145
1146
1147
1148
1149
def __init__(self, polyline: Polyline, ref: SpatialReference | None = None) -> None:
    self.ref = ref or polyline.spatialReference
    polyline = polyline.projectAs(ref) if ref and ref != polyline.spatialReference else polyline
    self._orig_polyline = polyline
    self.polyline = polyline

centroid property

centroid: PointGeometry

Centroid of the feature if it is within the feature (same as measure@50%)

first_point property writable

first_point: PointGeometry

PointGeometry of the Polyline's firstPoint

last_point property writable

last_point: PointGeometry

PointGeometry of the Polyline's lastPoint

original property

original: Polyline

The original Polyline given to the PolylineEditor

part_editors property writable

part_editors: list[PolylineEditor]

Get PolylineEditors for each part in the line

parts property writable

parts: list[Polyline]

A list of parts of the Polyline as Polylines (singlepart polylines will contain one part)

segments property

segments: list[Polyline]

Get all 2-point segments that make up the line

true_centroid property

true_centroid: PointGeometry

Center of gravity of the feature (not always in the line)

add_point

add_point(
    point: PointGeometry, snap: bool = False
) -> int | None

Insert a point in the polyline determining the ideal index based on measure

PARAMETER DESCRIPTION

point

The point to insert

TYPE: PointGeometry

snap

Snap the point to the line before adding it (default: False)

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
int | None

The index of the newly added point or None if the point is already in the line

Source code in src/arcpie/utils.py
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
def add_point(self, point: PointGeometry, snap: bool = False) -> int | None:
    """Insert a point in the polyline determining the ideal index based on measure

    Args:
        point: The point to insert
        snap: Snap the point to the line before adding it (default: False)

    Returns:
        The index of the newly added point or `None` if the point is already in the line
    """
    pl = self.polyline
    if point.spatialReference != self.ref:
        point = point.projectAs(self.ref)
    point = pl.snapToLine(point) if snap else point
    pt_measure = pl.measureOnLine(point)
    for idx, pt in enumerate(self):
        if pl.measureOnLine(pt) > pt_measure:
            self.insert(idx, point)
            return idx

add_points

add_points(
    points: Iterable[PointGeometry], snap: bool = False
) -> list[int | None]

Insert points into the line based on measure

PARAMETER DESCRIPTION

points

The points to insert

TYPE: Iterable[PointGeometry]

snap

Snap the points to the line before adding them (default: False)

TYPE: bool DEFAULT: False

Source code in src/arcpie/utils.py
1654
1655
1656
1657
1658
1659
1660
1661
def add_points(self, points: Iterable[PointGeometry], snap: bool = False) -> list[int | None]:
    """Insert points into the line based on measure

    Args:
        points: The points to insert
        snap: Snap the points to the line before adding them (default: False)
    """
    return [self.add_point(point, snap) for point in points]

align

align(
    line: Polyline | PolylineEditor,
    *,
    tolerance: float = 0.01,
) -> None

Adjust points of the polyline to be coincident with the target line of they are within tolerance units. If multiple points are within the tolerance, the closest is picked

Source code in src/arcpie/utils.py
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
def align(self, line: Polyline | PolylineEditor, *, tolerance: float = 0.01) -> None:
    """Adjust points of the polyline to be coincident with the target line of they are within `tolerance` units.
     If multiple points are within the tolerance, the closest is picked
    """
    if isinstance(line, Polyline):
        line = PolylineEditor(line)
    other_points = list(line)
    for idx, point in enumerate(self):
        nearest = min(other_points, key=lambda p: p.distanceTo(point))
        if nearest.distanceTo(point) <= tolerance:
            self[idx] = nearest

append

append(point: Point | PointGeometry) -> None

Append a point to the last part of the Polyline

Source code in src/arcpie/utils.py
1375
1376
1377
1378
1379
1380
1381
def append(self, point: Point | PointGeometry) -> None:
    """Append a point to the last part of the Polyline"""
    points = list(self.part_editors[-1])
    points.append(self._cast_point(point))
    parts = self.parts[:-1]
    parts.append(self.from_points(points, self.ref))
    self.polyline = self.merge_lines(parts)

append_part

append_part(part: Polyline | PolylineEditor) -> None

Add a part to the polyline

Source code in src/arcpie/utils.py
1466
1467
1468
1469
1470
1471
1472
1473
1474
def append_part(self, part: Polyline | PolylineEditor) -> None:
    """Add a part to the polyline"""
    parts = self.part_editors
    if isinstance(part, PolylineEditor):
        new_parts = part.part_editors
    else:
        new_parts = PolylineEditor(part).part_editors
    parts.extend(new_parts)
    self.polyline = self.merge_lines(p.polyline for p in parts)

copy

copy() -> PolylineEditor

Return a copy of the current editor with the active polyline state as the original polyline

Source code in src/arcpie/utils.py
1408
1409
1410
def copy(self) -> PolylineEditor:
    """Return a copy of the current editor with the active polyline state as the original polyline"""
    return PolylineEditor(self.polyline)

count

count(point: Point | PointGeometry) -> int

Get the count of points in the line that match the input point

Source code in src/arcpie/utils.py
1403
1404
1405
1406
def count(self, point: Point | PointGeometry) -> int:
    """Get the count of points in the line that match the input point"""
    point = self._cast_point(point)
    return list(self).count(point)

dedupe

dedupe(keep: Literal['first', 'last'] = 'last') -> int

Remove all duplicate points in a line keeping last instance (returns the number of points removed)

Note

If a duplciate is found in a seperate part, it is not considered a duplicate and will be kept setting keep to 'last' will remove duplicates from the end of the line first

Source code in src/arcpie/utils.py
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
def dedupe(self, keep: Literal['first', 'last'] = 'last') -> int:
    """Remove all duplicate points in a line keeping last instance (returns the number of points removed)

    Note:
        If a duplciate is found in a seperate part, it is not considered a duplicate and will be kept
         setting `keep` to `'last'` will remove duplicates from the end of the line first
    """
    parts = list(self.part_editors)
    removed = 0
    for part in parts:
        points = list(part)
        if keep == 'first':
            points.reverse()

        for point in filter(lambda point: points.count(point) > 1, points):
            points.remove(point)
            removed += 1

        if keep == 'first':
            points.reverse()
        part.polyline = self.from_points(points, self.ref)

    self.polyline = self.merge_lines(p.polyline for p in parts)
    return removed

discard

discard(point: Point | PointGeometry) -> None

Silently remove points without raising an error if it doesn't exist

Source code in src/arcpie/utils.py
1416
1417
1418
1419
1420
1421
def discard(self, point: Point | PointGeometry) -> None:
    """Silently remove points without raising an error if it doesn't exist"""
    try:
        self.remove(point)
    except IndexError:
        return

extend

extend(points: Iterable[Point | PointGeometry]) -> None

Extend the last part of the polyline with the points

Source code in src/arcpie/utils.py
1383
1384
1385
1386
1387
1388
1389
def extend(self, points: Iterable[Point | PointGeometry]) -> None:
    """Extend the last part of the polyline with the points"""
    self_points = list(self.part_editors[-1])
    self_points.extend(self._cast_point(p) for p in points)
    parts = self.parts[:-1]
    parts.append(self.from_points(self_points, self.ref))
    self.polyline = self.merge_lines(parts)

generalize

generalize(distance: float) -> None

Generalize the polyline

Source code in src/arcpie/utils.py
1462
1463
1464
def generalize(self, distance: float) -> None:
    """Generalize the polyline"""
    self.polyline = self.polyline.generalize(distance)

index

index(
    point: Point | PointGeometry,
    start: SupportsIndex = 0,
    stop: SupportsIndex | None = None,
) -> int

Get the global index of the first instance of the specified point, raise a ValueError if the point is not in the Polyline

Source code in src/arcpie/utils.py
1368
1369
1370
1371
1372
1373
def index(self, point: Point | PointGeometry, start: SupportsIndex = 0, stop: SupportsIndex | None = None) -> int:
    """Get the global index of the first instance of the specified point, raise a ValueError if the point is not in the Polyline"""
    for idx, p in enumerate(list(self)[start:stop]):
        if not point.disjoint(p):
            return idx
    raise IndexError(f'Point {point} not found in polyline')

insert

insert(
    index: SupportsIndex, point: Point | PointGeometry
) -> None

Insert a point at the specified index

Note

Inserts the point at the global point index, whichever part that may be in If you want to insert a point at a specific part index, use .parts to access the part and then insert the point there. This does not apply to single part features since the local part index is equal to the global index.

Source code in src/arcpie/utils.py
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
def insert(self, index: SupportsIndex, point: Point | PointGeometry) -> None:
    """Insert a point at the specified index

    Note:
        Inserts the point at the global point index, whichever part that may be in
         If you want to insert a point at a specific part index, use `.parts` to access the
         part and then insert the point there. This does not apply to single part features since
         the local part index is equal to the global index.
    """
    point = self._cast_point(point)
    part_idx, local_idx = self._part_at_index(index)
    parts = list(self.parts)
    new_part = list(self.part_editors[part_idx])
    new_part.insert(local_idx, point)
    parts[part_idx] = self.from_points(new_part, self.ref)
    self.polyline = self.merge_lines(parts)

intersections

intersections(
    other: Polyline | PolylineEditor,
) -> Iterator[PointGeometry]

Iterable of Point Intersections between this line and the other

Note: Intersections are de-duplicated and returned as PointGeometry objects

Source code in src/arcpie/utils.py
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
def intersections(self, other: Polyline | PolylineEditor) -> Iterator[PointGeometry]:
    """Iterable of Point Intersections between this line and the other

    Note: Intersections are de-duplicated and returned as PointGeometry objects
    """
    if isinstance(other, PolylineEditor):
        other = other.polyline

    if other.disjoint(self.polyline):
        return

    intersection = self.polyline.intersect(other, 1)
    if isinstance(intersection, PointGeometry) and intersection.isMultipart:
        intersection = Multipoint(Array(list(intersection)), self.polyline.spatialReference)

    if isinstance(intersection, Multipoint):
        seen: list[PointGeometry] = []
        for p in (PointGeometry(p, self.polyline.spatialReference) for p in intersection):
            if not any(p == s for s in seen):
                yield p
                seen.append(p)
    else:
        yield PointGeometry(intersection.centroid, self.polyline.spatialReference)

merge_lines classmethod

merge_lines(lines: Iterable[Polyline]) -> Polyline

Merge a sequence of Polylines into one Polyline (uses union so each line becomes a part)

Example
    lines = [[a,b,c,d], [e,f,g,h]]
    new = PolylineEditor.merge_lines(lines)
    new == Polyline([p1[a,b,c,d], p2[e,f,g,h]])
Source code in src/arcpie/utils.py
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
@classmethod
def merge_lines(cls, lines: Iterable[Polyline]) -> Polyline:
    """Merge a sequence of Polylines into one Polyline (uses `union` so each line becomes a part)

    Example:
        ```python
            lines = [[a,b,c,d], [e,f,g,h]]
            new = PolylineEditor.merge_lines(lines)
            new == Polyline([p1[a,b,c,d], p2[e,f,g,h]])
        ```
    """
    # Merge algorithm that keeps sequential touching lines in the same part,
    # but allows for disjoint parts to remain disjoint
    parts: list[list[PointGeometry]] = []
    last_point: PointGeometry | None = None
    ref: SpatialReference | None = None
    for line in lines:
        if ref is None:
            ref = line.spatialReference
        points = list(cls(line))
        if last_point and not points[0].disjoint(last_point):
            parts[-1].extend(points)
        else:
            parts.append(points)
        last_point = points[-1]
    return Polyline(
        Array(
            Array(p.centroid for p in part)
            for part in parts
        ),
        ref
    )

move

move(vec: Vector) -> None

Move the polyline along a Vector

Source code in src/arcpie/utils.py
1485
1486
1487
def move(self, vec: Vector) -> None:
    """Move the polyline along a Vector"""
    self.polyline = vec.move(self.polyline)

offset

offset(
    dist: float, side: Literal['both'] = 'both'
) -> tuple[Polyline, Polyline]
offset(
    dist: float, side: Literal['left', 'right']
) -> Polyline
offset(
    dist: float,
    side: Literal['left', 'right', 'both'] = 'both',
) -> Polyline | tuple[Polyline, Polyline]

Offset the line in the provided direction

PARAMETER DESCRIPTION

dist

The distance to offet by (feature units)

TYPE: float

side

The side of the line to offset to

TYPE: Literal['left', 'right', 'both'] DEFAULT: 'both'

Source code in src/arcpie/utils.py
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
def offset(self, dist: float, side: Literal['left', 'right', 'both'] = 'both') -> Polyline | tuple[Polyline, Polyline]:
    """Offset the line in the provided direction

    Args:
        dist: The distance to offet by (feature units)
        side: The side of the line to offset to
    """
    ref = self.ref
    left_points = list[PointGeometry]()
    right_points = list[PointGeometry]()
    pi_2 = round(math.pi / 2, 15)
    for p1, p2 in itertools.pairwise(self):
        seg_vec = Vector(p1, p2, ref)
        left_vec = seg_vec.rotate(pi_2).norm() * dist
        right_vec = seg_vec.rotate(-pi_2).norm() * dist
        if not left_points:
            left_points.append(left_vec >> p1)
        if not right_points:
            right_points.append(right_vec >> p1)

        right_points.append(right_vec >> p2)
        left_points.append(left_vec >> p2)

    left, right = (
        PolylineEditor.from_points(left_points, ref),
        PolylineEditor.from_points(right_points, ref)
    )
    if side == 'both':
        return left, right
    if side == 'left':
        return left
    if side == 'right':
        return right

    raise ValueError(f'{side} is not one of left, right, both')

orienent_with

orienent_with(line: Polyline | PolylineEditor) -> None

Alter the direction of the polyline to match the direction of the input line (only works for lines that overlap)

Source code in src/arcpie/utils.py
1535
1536
1537
1538
1539
1540
1541
1542
def orienent_with(self, line: Polyline | PolylineEditor) -> None:
    """Alter the direction of the polyline to match the direction of the input line (only works for lines that overlap)"""
    if isinstance(line, PolylineEditor):
        line = line.polyline

    if not line.disjoint(self.polyline) and (int_part := line.intersect(self.polyline, 2)):
        if self.polyline.measureOnLine(int_part.firstPoint) > self.polyline.measureOnLine(int_part.lastPoint):
            self.reverse()

pop

pop(index: SupportsIndex = -1) -> PointGeometry

Remove a point from a polyline at the specified index (default: -1)

Source code in src/arcpie/utils.py
1341
1342
1343
1344
1345
1346
1347
1348
1349
def pop(self, index: SupportsIndex = -1, /) -> PointGeometry:
    """Remove a point from a polyline at the specified index (default: -1)"""
    part_idx, local_idx = self._part_at_index(index)
    parts = list(self.parts)
    new_part = list(self.part_editors[part_idx])
    pt = new_part.pop(local_idx)
    parts[part_idx] = self.from_points(new_part, self.ref)
    self.polyline = self.merge_lines(parts)
    return pt

pop_part

pop_part(index: SupportsIndex = -1) -> Polyline

pop a part from the polyline (default: -1)

Source code in src/arcpie/utils.py
1476
1477
1478
1479
1480
1481
1482
1483
def pop_part(self, index: SupportsIndex = -1, /) -> Polyline:
    """pop a part from the polyline (default: -1)"""
    parts = self.part_editors
    if len(parts) == 1:
        raise ValueError('cannot pop parts from a single part polyline')
    part = parts.pop(index)
    self.polyline = self.merge_lines(p.polyline for p in parts)
    return part.polyline

project_as

project_as(ref: SpatialReference | int) -> None

Project the polyline in the given reference

Source code in src/arcpie/utils.py
1529
1530
1531
1532
1533
def project_as(self, ref: SpatialReference | int) -> None:
    """Project the polyline in the given reference"""
    if isinstance(ref, int):
        ref = SpatialReference(ref)
    self.polyline = self.polyline.projectAs(ref)

remove

remove(point: Point | PointGeometry) -> None

Remove the fist occurrence of the point from the polyline

Source code in src/arcpie/utils.py
1412
1413
1414
def remove(self, point: Point | PointGeometry) -> None:
    """Remove the fist occurrence of the point from the polyline"""
    self.pop(self.index(point))

reset

reset() -> None

Revert all changes to polyline and restore the original geometry

Source code in src/arcpie/utils.py
1450
1451
1452
def reset(self) -> None:
    """Revert all changes to `polyline` and restore the original geometry"""
    self.polyline = self._orig_polyline

reverse

reverse() -> None

Reverse the polyline parts and the points of each part of the polyline

Example

[[a, b], [c, d]] -> [[d, c], [b, a]][a, b, c, d] -> [d, c, b, a]

Source code in src/arcpie/utils.py
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
def reverse(self) -> None:
    """Reverse the polyline parts and the points of each part of the polyline

    Example:
        [[a, b], [c, d]] -> [[d, c], [b, a]]
        [a, b, c, d] -> [d, c, b, a]
    """
    # self.polyline = self.merge_lines(self.from_points(reversed(list(part))) for part in reversed(self.part_editors))
    rev = self.polyline.reverseOrientation()  # type: ignore
    assert isinstance(rev, Polyline)
    self.polyline = rev

split_at_angle

split_at_angle(
    ang: float,
    units: Literal['degrees', 'radians'] = 'radians',
    *,
    tolerance: float = 0.1,
) -> list[Polyline]

Split the polyline at points where the instantaneous angle (radians) is greater than the specified angle (radians)

PARAMETER DESCRIPTION

ang

The target angle at a point to be split at

TYPE: float

units

Specify the units of the provided angle and tolerance

TYPE: Literal['degrees', 'radians'] DEFAULT: 'radians'

tolerance

+/- of target angle to trigger a split (default: 0.1rad/~5.7deg)

TYPE: float DEFAULT: 0.1

Source code in src/arcpie/utils.py
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
def split_at_angle(self, ang: float, units: Literal['degrees', 'radians'] = 'radians', *, tolerance: float = 0.1) -> list[Polyline]:
    """Split the polyline at points where the instantaneous angle (radians) is greater than the specified angle (radians)

    Args:
        ang: The target angle at a point to be split at
        units: Specify the units of the provided angle and tolerance
        tolerance: +/- of target angle to trigger a split (default: `0.1rad`/`~5.7deg`)
    """
    ang = abs(ang)
    tolerance = abs(tolerance)
    if units == 'degrees':
        ang = abs(math.radians(ang) % math.pi)
        tolerance = abs(math.radians(tolerance))

    segs: list[Polyline] = []
    for part in self.part_editors:
        last_split = 0
        for idx, point in enumerate(part[1:-1], start=1):
            left, right = vectors_at(part.polyline, point)
            if abs(left @ right) < ang + tolerance:
                segs.append(self.from_points(part[last_split:idx + 1], self.ref))
                last_split = idx

        if last_split != len(part) - 1:
            segs.append(self.from_points(part[last_split:], self.ref))

    if self.polyline.hasCurves:
        # Use segment measure to preserve curves
        segs = [
            self.polyline.segmentAlongLine(
                self.polyline.measureOnLine(seg.firstPoint),
                self.polyline.measureOnLine(seg.lastPoint)
            )
            for seg in segs
        ]
    return segs

split_at_point

split_at_point(
    point: Point | PointGeometry, snap: Literal[True] = ...
) -> tuple[Polyline, Polyline] | Polyline
split_at_point(
    point: Point | PointGeometry, snap: Literal[False] = ...
) -> tuple[Polyline, Polyline] | Polyline | None
split_at_point(
    point: Point | PointGeometry, snap: bool = False
) -> tuple[Polyline, Polyline] | Polyline | None

Split the Polyline at a point returning two Polylines

PARAMETER DESCRIPTION

point

The Point to split the line at

TYPE: Point | PointGeometry

snap

Snap the point to the line before splitting (default: False)

TYPE: bool DEFAULT: False

Note

If the point is at the line extremeties, the polyline will be returned

Source code in src/arcpie/utils.py
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
def split_at_point(self, point: Point | PointGeometry, snap: bool = False) -> tuple[Polyline, Polyline] | Polyline | None:
    """Split the Polyline at a point returning two Polylines

    Args:
        point: The Point to split the line at
        snap: Snap the point to the line before splitting (default: `False`)

    Note:
        If the point is at the line extremeties, the polyline will be returned
    """
    pl = self.polyline
    point = self._cast_point(point)
    point = pl.snapToLine(point) if snap else point
    if point.touches(pl):
        return pl
    measure = pl.measureOnLine(point)
    return pl.segmentAlongLine(0, measure), pl.segmentAlongLine(measure, pl.length)

split_at_points

split_at_points(
    points: Iterable[Point | PointGeometry],
    snap: bool = False,
) -> list[Polyline]

Split the line at the provided points

PARAMETER DESCRIPTION

points

The Points to split the line at

TYPE: Iterable[Point | PointGeometry]

snap

Snap the points to the line before splitting (default: False)

TYPE: bool DEFAULT: False

Source code in src/arcpie/utils.py
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
def split_at_points(self, points: Iterable[Point | PointGeometry], snap: bool = False) -> list[Polyline]:
    """Split the line at the provided points

    Args:
        points: The Points to split the line at
        snap: Snap the points to the line before splitting (default: `False`)
    """
    pl = self.polyline
    line = PolylineEditor(self.polyline, self.ref)
    segments = list[Polyline]()
    points = sorted(points, key=lambda p: pl.measureOnLine(p))
    for point in points:
        parts = line.split_at_point(point, snap)
        if isinstance(parts, tuple):
            segments.append(parts[0])
            line.polyline = parts[1]
    segments.append(line.polyline)
    return segments

Vector dataclass

Vector(
    tail: Point | PointGeometry,
    head: Point | PointGeometry,
    ref: SpatialReference | None = None,
)

Simple Vector implementation that takes a start and end point (uses Spherical notation for theta and phi).

If PointGeometries are passed as the head and tail points, the head will inherit the reference of the tail

ATTRIBUTE DESCRIPTION
x

The X component of the vector

TYPE: float

y

The Y component of the vector

TYPE: float

z

The Z component of the vector

TYPE: float

theta

The angle between the vector and the x-axis

TYPE: float

dist

The magnitute of the vector (distance b/w start and end)

TYPE: float

mid

The midpoint of the vector along its magnitude

TYPE: Point

ref

Ano optional spatial reference for all Vector geometry to inherit

TYPE: SpatialReference | None

METHOD DESCRIPTION
translate

Translate a point along the Vector (Vector tail is set to point location)

reverse

Reverse the Vector

add

Vector addition (LHS is origin)

subtract

Vector subtraction (LHS is origin)

dot

Dot product of two vectors (LHS is origin)

cross

Cross product of two vectors (LHS is origin)

scale

Scale a vector using a scalar

angle

Get angle between two vectors

__rshift__

Implements >> for use in translating points (vector must be LHS)

__neg__

Implements unary - operator for Vectors. Same as .reverse()

__add__

Implements + operator for Vectors. RHS Vector will be used as anchor

__sub__

Implements - operator for Vectors. RHS Vector will be used as anchor

__xor__

Implements ^ operator for Vectors (dot product).

__mul__

Implements * operator for Vectors. RHS can be Vector (cross product) or Scalar

__matmul__

Implements @ to determine accute angle between two vectors

bearing property

bearing: float

Angle of the vector

head_geom property

head_geom: PointGeometry

Get a point geometry object for the vector head (end)

mid_geom property

mid_geom: PointGeometry

Get a point geometry object for the vector midpoint (inherits reference from head/tail)

tail_geom property

tail_geom: PointGeometry

Get a point geometry object for the vector tail (start)

add

add(other: Vector) -> Vector

Vector addition originating at LHS (+)

Source code in src/arcpie/utils.py
950
951
952
def add(self, other: Vector) -> Vector:
    """Vector addition originating at LHS (`+`)"""
    return Vector(self.tail, other >> self.head, self.ref)

angle

angle(other: Vector) -> float

Get the angle in radians between two vectors

Note: When checking angle against a null vector, 0 is returned

Source code in src/arcpie/utils.py
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
def angle(self, other: Vector) -> float:
    """Get the angle in radians between two vectors

    Note: When checking angle against a null vector, 0 is returned
    """
    if self.is_null or other.is_null:
        return 0

    other = Vector(self.tail, other >> self.tail, self.ref)
    v = round((self ^ other) / (self.dist * other.dist), 15)
    ang = round(math.acos(v), 15)
    return ang

cross

cross(other: Vector) -> Vector

Cross product of two vectors originating at LHS (*)

Source code in src/arcpie/utils.py
964
965
966
967
968
969
970
971
972
973
974
975
976
def cross(self, other: Vector) -> Vector:
    """Cross product of two vectors originating at LHS (`*`)"""
    if self.is_null:
        return self
    if other.is_null:  # Null vector at own tail
        return Vector(self.tail, self.tail, self.ref)
    other = Vector(self.tail, other >> self.tail, self.ref)
    targ = self.tail_geom.move(
        dx=self.y * other.z - self.z * other.y,
        dy=self.z * other.x - self.x * other.z,
        dz=(self.x * other.y - self.y * other.x) if self.z else None,
    )
    return Vector(self.tail, targ, self.ref)

dot

dot(other: Vector) -> float

Dot product of two vectors originating at LHS (@)

Source code in src/arcpie/utils.py
 997
 998
 999
1000
def dot(self, other: Vector) -> float:
    """Dot product of two vectors originating at LHS (`@`)"""
    other = Vector(self.tail, other >> self.tail, self.ref)
    return self.x * other.x + self.y * other.y + self.z * other.z

norm

norm() -> Vector

Normal vector originating at tail

Source code in src/arcpie/utils.py
939
940
941
def norm(self) -> Vector:
    """Normal vector originating at tail"""
    return Vector(self.tail, self.translate(self.tail, 1), self.ref)

reverse

reverse() -> Vector

Reversed vector

Source code in src/arcpie/utils.py
932
933
934
def reverse(self) -> Vector:
    """Reversed vector"""
    return Vector(self.head, self.tail, self.ref)

rotate

rotate(angle: float) -> Vector

Rotate the vector counter-clockwise around the tail (2D) Args: angle: The angle in radians to rotate the vector by

Source code in src/arcpie/utils.py
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
def rotate(self, angle: float) -> Vector:
    """Rotate the vector counter-clockwise around the tail (2D)
    Args:
        angle: The angle in `radians` to rotate the vector by
    """
    unit = self.unit()
    head = unit.head_geom.centroid
    cos_theta = math.cos(angle)
    sin_theta = math.sin(angle)
    x = head.X * cos_theta - head.Y * sin_theta
    y = head.Y * cos_theta + head.X * sin_theta
    rot = Vector(Point(0, 0), Point(x, y), self.ref)
    return Vector(self.tail, rot.translate(self.tail, self.dist), self.ref)

rotate_degrees

rotate_degrees(angle: float) -> Vector

Alternate method for rotate that accepts an angle in degrees

Source code in src/arcpie/utils.py
1120
1121
1122
def rotate_degrees(self, angle: float) -> Vector:
    """Alternate method for rotate that accepts an angle in degrees"""
    return self.rotate(math.radians(angle % 360))

scale

scale(scale: Scalar) -> Vector

Scalar multiplication of vector (*)

Note: Scaling by Zero will return a null vector located at the vector tail

Source code in src/arcpie/utils.py
978
979
980
981
982
983
984
985
def scale(self, scale: Scalar) -> Vector:
    """Scalar multiplication of vector (`*`)

    Note: Scaling by Zero will return a null vector located at the vector tail
    """
    if scale == 0:  # Special case for creating a spatially aware null vector
        return Vector(self.tail, self.tail, self.ref)
    return Vector(self.tail, self.translate(self.tail, scale * self.dist), self.ref)

subtract

subtract(other: Vector) -> Vector

Vector subtraction originating at LHS (-)

Source code in src/arcpie/utils.py
957
958
959
def subtract(self, other: Vector) -> Vector:
    """Vector subtraction originating at LHS (`-`)"""
    return self + (-other)

translate

translate(point: Point, mag: float | None = None) -> Point
translate(
    point: PointGeometry, mag: float | None = None
) -> PointGeometry
translate(point: Any, mag: float | None = None) -> Any

Translate the provided point along the vector direction. >>

The Point will be moved from its original location along the vector angle the provided distance. The location of the Vector object is not taken into account, only angle and magnitude

PARAMETER DESCRIPTION

point

The point to translate along the given vector

TYPE: Any

mag

The distance to translate the point (default: self.mag)

TYPE: float | None DEFAULT: None

RETURNS DESCRIPTION
Any

A translated Point/PointGeometry

Example
    trans_point = vec >> point
Source code in src/arcpie/utils.py
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
def translate(self, point: Any, mag: float | None = None) -> Any:
    """Translate the provided point along the vector direction. `>>`

    The Point will be moved from its original location along the vector angle the provided distance.
    The location of the `Vector` object is not taken into account, only angle and magnitude

    Args:
        point: The point to translate along the given vector
        mag: The distance to translate the point (default: `self.mag`)

    Returns:
        A translated Point/PointGeometry

    Example:
        ```python
            trans_point = vec >> point
        ```
    """

    if self.is_null or mag == 0:
        return point

    if not isinstance(point, (Point, PointGeometry)):
        raise TypeError(f'point must be Point or PointGeometry not {type(point)}')

    target = point
    ref = self.ref

    if isinstance(target, PointGeometry):
        ref = target.spatialReference
        target = target.centroid

    mag = mag or self.dist
    target = Point(
        target.X + (mag * self.x) / self.dist,
        target.Y + (mag * self.y) / self.dist,
        target.Z + (mag * self.z) / self.dist if target.Z is not None else None,
        target.M,
        target.ID,
    )

    return (
        PointGeometry(
            target,
            ref,
            has_z=target.Z is not None,
            has_m=target.M is not None,
            has_id=bool(target.ID),
        )
        if isinstance(point, PointGeometry)
        else target
    )

unit

unit() -> Vector

Return a unit vector originating at the origin

Source code in src/arcpie/utils.py
943
944
945
def unit(self) -> Vector:
    """Return a unit vector originating at the origin"""
    return Vector(Point(0, 0), self.translate(Point(0, 0), 1), self.ref)

box_on_point

box_on_point(
    center: Point | PointGeometry,
    width: float,
    height: float,
    angle: float = 0.0,
    ref: SpatialReference | None = None,
    start: Literal['tl', 'tr', 'bl', 'br'] = 'tl',
) -> Polygon

Build a rectangular box on a point

PARAMETER DESCRIPTION

center

The center point of the box

TYPE: Point | PointGeometry

width

The width of the box

TYPE: float

height

The height of the box

TYPE: float

angle

An angle to roatate the box by in radians (default: 0.0)

TYPE: float DEFAULT: 0.0

ref

An optional spatial reference to apply to the output polygon

TYPE: SpatialReference | None DEFAULT: None

start

The corner of the box that should be the start point (default: 'tl')

TYPE: Literal['tl', 'tr', 'bl', 'br'] DEFAULT: 'tl'

RETURNS DESCRIPTION
Polygon

A rectangular polygon (with provided ref or ref inhereted from center)

Source code in src/arcpie/utils.py
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
def box_on_point(
    center: Point | PointGeometry,
    width: float, height: float,
    angle: float = 0.0,
    ref: SpatialReference | None = None,
    start: Literal['tl', 'tr', 'bl', 'br'] = 'tl'
    ) -> Polygon:
    """Build a rectangular box on a point

    Args:
        center (Point|PointGeometry): The center point of the box
        width (float): The width of the box
        height (float): The height of the box
        angle (float): An angle to roatate the box by in radians (default: 0.0)
        ref (SpatialReference|None): An optional spatial reference to apply to the output polygon
        start (Literal['tl', 'tr', 'bl', 'br']): The corner of the box that should be the start point (default: 'tl')

    Returns:
        (Polygon): A rectangular polygon (with provided ref or ref inhereted from center)
    """
    if isinstance(center, PointGeometry):
        ref = ref or center.spatialReference
        if center.spatialReference != ref:
            center = center.projectAs(ref)
        center = center.centroid

    h_width = width / 2
    h_height = height / 2
    tl = Point(center.X - h_width, center.Y + h_height)
    tr = Point(center.X + h_width, center.Y + h_height)
    bl = Point(center.X - h_width, center.Y - h_height)
    br = Point(center.X + h_width, center.Y - h_height)

    if start == 'tr':
        points = [br, bl, tl, tr]
    elif start == 'br':
        points = [bl, tl, tr, br]
    elif start == 'bl':
        points = [tl, tr, br, bl]
    elif start == 'tl':
        points = [tr, br, bl, tl]

    box = Polygon(Array(points), spatial_reference=ref)
    if angle:
        box = box.rotate(center, angle)  # type: ignore
        assert isinstance(box, Polygon)
    return box

center_circle

center_circle(
    center: Point | PointGeometry,
    radius: float,
    ref: SpatialReference | None = None,
) -> Polyline

Create a circle using a center point and a radius

PARAMETER DESCRIPTION

center

The center of the circle

TYPE: Point | PointGeometry

radius

(float): The dist

TYPE: float

ref

(SpatialReference|None): The SpatialReference to use with the returned geometry

TYPE: SpatialReference | None DEFAULT: None

RETURNS DESCRIPTION
Polyline

A Circular Polyline

Note

If a PointGeometry are provided, it will be projected as the provided ref If no ref is provided, the shape will inherit the reference of the center

Reference resolution is as follows: ref -> center.spatialReference

If no center reference can be found and no ref is provided, the returned geometry will have no reference

Source code in src/arcpie/utils.py
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
def center_circle(center: Point | PointGeometry, radius: float, ref: SpatialReference | None = None) -> Polyline:
    """Create a circle using a center point and a radius

    Args:
        center (Point|PointGeometry): The center of the circle
        radius: (float): The dist
        ref: (SpatialReference|None): The SpatialReference to use with the returned geometry

    Returns:
        (Polyline): A Circular Polyline

    Note:
        If a PointGeometry are provided, it will be projected as the provided ref
        If no ref is provided, the shape will inherit the reference of the center

        Reference resolution is as follows:
        `ref -> center.spatialReference`

        If no center reference can be found and no ref is provided, the returned geometry will
         have no reference
    """
    if isinstance(center, PointGeometry):
        ref = ref or center.spatialReference
        center = center.centroid
    return two_point_circle(center, Point(center.X, center.Y + radius), ref)

convert_schema

convert_schema(
    schema: Dataset[Any] | Path | str,
    to: Literal[
        'JSON', 'XLSX', 'HTML', 'PDF', 'XML', 'DYNAMIC_HTML'
    ] = 'JSON',
) -> BytesIO

Convert a Schema from one format to another

PARAMETER DESCRIPTION

schema

Path to the schemafile or Dataset to convert

TYPE: Dataset | Path | str

to

Target format (default: 'JSON')

TYPE: Literal['JSON', 'XLSX', 'HTML', 'PDF', 'XML', 'DYNAMIC_HTML'] DEFAULT: 'JSON'

YIELDS DESCRIPTION
bytes

Raw bytes object containing the schema file

TYPE:: BytesIO

Source code in src/arcpie/utils.py
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
def convert_schema(schema: Dataset[Any] | Path | str, to: Literal['JSON', 'XLSX', 'HTML', 'PDF', 'XML', 'DYNAMIC_HTML'] = 'JSON') -> BytesIO:
    """Convert a Schema from one format to another

    Args:
        schema (Dataset|Path|str): Path to the schemafile or Dataset to convert
        to (Literal['JSON', 'XLSX', 'HTML', 'PDF', 'XML', 'DYNAMIC_HTML']): Target format (default: 'JSON')

    Yields:
        bytes: Raw bytes object containing the schema file
    """
    with TemporaryDirectory(suffix=to) as temp:
        temp = Path(temp)
        if not isinstance(schema, (Path, str)):
            # Convert Dataset to report
            schema, = GenerateSchemaReport(str(schema.conn), str(temp), 'json_schema', 'JSON')
        schema = Path(schema)
        conversion, = ConvertSchemaReport(str(schema), str(temp), 'out', to)
        return BytesIO(Path(conversion).read_bytes())

export_project_lyrx

export_project_lyrx(
    project: Project,
    out_dir: Path,
    *,
    indent: int = 4,
    sort: bool = False,
    skip_empty: bool = True,
) -> None

Pull all layers from a project file and output them in a directory as lyrx files

PARAMETER DESCRIPTION

project

The arcpie.Project instance to export

TYPE: Project

out_dir

The target directory for the layer files

TYPE: Path | str

indent

Indentation level of the ouput files (default: 4)

TYPE: int DEFAULT: 4

sort

Sort the output file by key name (default: False)

TYPE: bool DEFAULT: False

skip_empty

Skips writing empty lyrx files for layers with no lyrx data (default: True)

TYPE: bool DEFAULT: True

Usage
>>> export_project_lyrx(arcpie.Project('<path/to/aprx>'), '<path/to/output_dir>')
Note

Output structure will match the structure of the project: Map -> Group -> Layer Where each level is a directory. Group Layers will have a directory entry with individual files for each layer they contain, as well as a single layerfile that contains all their child layers.

Source code in src/arcpie/utils.py
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def export_project_lyrx(project: Project, out_dir: Path, *, indent: int = 4, sort: bool = False, skip_empty: bool = True) -> None:
    """Pull all layers from a project file and output them in a directory as lyrx files

    Args:
        project (Project): The `arcpie.Project` instance to export
        out_dir (Path|str): The target directory for the layer files
        indent (int): Indentation level of the ouput files (default: 4)
        sort (bool): Sort the output file by key name (default: False)
        skip_empty (bool): Skips writing empty lyrx files for layers with no lyrx data (default: True)

    Usage:
        ```python
        >>> export_project_lyrx(arcpie.Project('<path/to/aprx>'), '<path/to/output_dir>')
        ```

    Note:
        Output structure will match the structure of the project:
        `Map -> Group -> Layer`
        Where each level is a directory. Group Layers will have a directory entry with individual
         files for each layer they contain, as well as a single layerfile that contains all their
         child layers.
    """
    out_dir = Path(out_dir)
    for map in project.maps:
        map_dir = out_dir / map.unique_name
        for layer in map.layers:
            lyrx = getattr(layer, 'lyrx', None)
            if lyrx is None:
                print(f'{(layer.cim_dict or {}).get("type")} is invalid!')
                continue
            if skip_empty and not lyrx:
                continue
            out_file = (map_dir / layer.name).with_suffix('.lyrx')
            out_file.parent.mkdir(parents=True, exist_ok=True)
            out_file.write_text(json.dumps(lyrx, indent=indent, sort_keys=sort), encoding='utf-8')

export_project_maps

export_project_maps(
    project: Project,
    out_dir: Path | str,
    *,
    indent: int = 4,
    sort: bool = False,
) -> None

Pull all layers from a project file and output them in a directory as mapx files

PARAMETER DESCRIPTION

project

The arcpie.Project instance to export

TYPE: Project

out_dir

The target directory for the mapx files

TYPE: Path | str

indent

Indentation level of the ouput files (default: 4)

TYPE: int DEFAULT: 4

sort

Sort the output file by key name (default: False)

TYPE: bool DEFAULT: False

Usage
>>> export_project_maps(arcpie.Project('<path/to/aprx>'), '<path/to/output_dir>')
Source code in src/arcpie/utils.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def export_project_maps(project: Project, out_dir: Path | str, *, indent: int = 4, sort: bool = False) -> None:
    """Pull all layers from a project file and output them in a directory as mapx files

    Args:
        project (Project): The `arcpie.Project` instance to export
        out_dir (Path|str): The target directory for the mapx files
        indent (int): Indentation level of the ouput files (default: 4)
        sort (bool): Sort the output file by key name (default: False)

    Usage:
        ```python
        >>> export_project_maps(arcpie.Project('<path/to/aprx>'), '<path/to/output_dir>')
        ```
    """
    out_dir = Path(out_dir)
    for map in project.maps:
        map_dir = out_dir / rf'{map.unique_name}'
        out_file = map_dir.with_suffix(f'{map_dir.suffix}.mapx')  # handle '.' in map name
        out_file.parent.mkdir(parents=True, exist_ok=True)
        out_file.write_text(json.dumps(map.mapx, indent=indent, sort_keys=sort), encoding='utf-8')

get_subtype_count

get_subtype_count(
    fc: Table | FeatureClass, drop_empty: bool = False
) -> dict[str, int]

Get the subtype counts for a Table or FeatureClass

PARAMETER DESCRIPTION

fc

The Table/FeatureClass you want subtype counts for

TYPE: Table | FeatureClass

drop_empty

Drop any counts that have no features from the output dictionary (default: False)

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
dict[str, int]

A mapping of subtype name to subtype count

Source code in src/arcpie/utils.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
def get_subtype_count(fc: Table | FeatureClass, drop_empty: bool = False) -> dict[str, int]:
    """Get the subtype counts for a Table or FeatureClass

    Args:
        fc (Table | FeatureClass): The Table/FeatureClass you want subtype counts for
        drop_empty (bool): Drop any counts that have no features from the output dictionary (default: False)

    Returns:
        (dict[str, int]): A mapping of subtype name to subtype count
    """
    return {
        subtype['Name']: cnt
        for code, subtype in fc.subtypes.items()
        if fc.subtype_field  # has Subtypes
        and (
            (cnt := count(fc[where(f'{fc.subtype_field} = {code}')]))  # Get count
            or drop_empty  # Drop Empty counts?
        )
    }

get_subtype_counts

get_subtype_counts(
    gdb: Dataset, *, drop_empty: bool = False
) -> dict[str, dict[str, int]]

Get a mapping of subtype counts for all featureclasses that have subtypes in the provided Dataset

PARAMETER DESCRIPTION

gdb

The Dataset instance to get subtype counts for

TYPE: Dataset

drop_empty

Drop any counts that have no features from the output dictionary (default: False)

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
dict[str, dict[str, int]]

A mapping of FeatureClass -> SubtypeName -> SubtypeCount

Usage
>>> get_subtype_counts(Dataset('<path/to/gdb>', drop_empty=True))
{
    'FC1':
        {
            'Default': 10
            'Subtype 1': 6
            ...
        },
    ...
}
Source code in src/arcpie/utils.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def get_subtype_counts(gdb: Dataset, *, drop_empty: bool = False) -> dict[str, dict[str, int]]:
    """Get a mapping of subtype counts for all featureclasses that have subtypes in the provided Dataset

    Args:
        gdb (Dataset): The Dataset instance to get subtype counts for
        drop_empty (bool): Drop any counts that have no features from the output dictionary (default: False)

    Returns:
        (dict[str, dict[str, int]]): A mapping of FeatureClass -> SubtypeName -> SubtypeCount

    Usage:
        ```python
        >>> get_subtype_counts(Dataset('<path/to/gdb>', drop_empty=True))
        {
            'FC1':
                {
                    'Default': 10
                    'Subtype 1': 6
                    ...
                },
            ...
        }
        ```
    """
    feats: list[Table] = [*gdb.feature_classes.values(), *gdb.tables.values()]
    return {
        fc.name: counts
        for fc in feats
        if (counts := get_subtype_count(fc))
        or not drop_empty
    }

iter_parts

iter_parts(
    line: Polyline, start: bool = True, end: bool = True
) -> Iterator[Iterator[PointGeometry]]

Get a part iterator for a Polyline

PARAMETER DESCRIPTION

line

The Polyline to iterate parts for

TYPE: Polyline

start

Include the line startpoint (default: True)

TYPE: bool DEFAULT: True

end

Include the line endpoint (default: True)

TYPE: bool DEFAULT: True

YIELDS DESCRIPTION
Iterator[PointGeometry]

Iterators of part PointGeometries for all parts in the line

Source code in src/arcpie/utils.py
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
def iter_parts(line: Polyline, start: bool = True, end: bool = True) -> Iterator[Iterator[PointGeometry]]:
    """Get a part iterator for a Polyline

    Args:
        line: The Polyline to iterate parts for
        start: Include the line startpoint (default: `True`)
        end: Include the line endpoint (default: `True`)

    Yields:
        Iterators of part PointGeometries for all parts in the line
    """
    yield from (
        iter_points(Polyline(part, line.spatialReference, start, end))
        for part in line
    )

iter_points

iter_points(
    line: Polyline, start: bool = True, end: bool = True
) -> Iterator[PointGeometry]

Get a point iterator for a Polyline

PARAMETER DESCRIPTION

line

The Polyline to iterate points for

TYPE: Polyline

start

Include the line startpoint (default: True)

TYPE: bool DEFAULT: True

end

Include the line endpoint (default: True)

TYPE: bool DEFAULT: True

Yields: PointGeometries for all points in the line

Source code in src/arcpie/utils.py
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
def iter_points(line: Polyline, start: bool = True, end: bool = True) -> Iterator[PointGeometry]:
    """Get a point iterator for a Polyline

    Args:
        line: The Polyline to iterate points for
        start: Include the line startpoint (default: `True`)
        end: Include the line endpoint (default: `True`)
    Yields:
        PointGeometries for all points in the line
    """
    section = slice(0 if start else 1, None if end else -1)
    ref = line.spatialReference
    yield from (
        PointGeometry(point, ref)
        for part in line
        for point in cast(list[Point], list(part))[section]
    )

nat

nat(val: str) -> tuple[tuple[int, ...], tuple[str, ...]]

Natural sort key for use in string sorting

PARAMETER DESCRIPTION

val

A value that you want the natural sort key for

TYPE: str

RETURNS DESCRIPTION
tuple[tuple[int, ...], tuple[str, ...]

A tuple containing all numeric and string components in order of appearance. Best used as a sort key.

Usage
>>> pages = ['P-1.3', 'P-2.11', ...]
>>> pages.sort(key=nat)
>>> print(pages)
['P-1.1', 'P-1.2', ...]
Source code in src/arcpie/utils.py
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def nat(val: str) -> tuple[tuple[int, ...], tuple[str, ...]]:
    """Natural sort key for use in string sorting

    Args:
        val (str): A value that you want the natural sort key for

    Returns:
        (tuple[tuple[int, ...], tuple[str, ...]): A tuple containing all numeric and
         string components in order of appearance. Best used as a sort key.

    Usage:
        ```python
        >>> pages = ['P-1.3', 'P-2.11', ...]
        >>> pages.sort(key=nat)
        >>> print(pages)
        ['P-1.1', 'P-1.2', ...]
        ```
    """
    digits: list[int] = []
    alpha: list[str] = []
    digit_chars: list[str] = []
    for s in val:
        if s.isdigit():
            digit_chars.append(s)
        else:
            alpha.append(s)
            if digit_chars:
                digits.append(int(''.join(digit_chars)))
                digit_chars.clear()
    if digit_chars:
        digits.append(int(''.join(digit_chars)))
    return tuple(digits), tuple(alpha)

patch_schema_rules

patch_schema_rules(
    schema: SchemaWorkspace | Path | str,
    *,
    remove_rules: bool = False,
) -> SchemaWorkspace

Patch an exported Schema doc by re-linking attribute rules to table names

PARAMETER DESCRIPTION

schema

The input schema to patch

TYPE: Path | str

remove_rules

Remove attribute rules from the schema (default: False)

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
SchemaWorkspace

A patched schema dictionary

TYPE: SchemaWorkspace

Source code in src/arcpie/utils.py
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
def patch_schema_rules(schema: SchemaWorkspace | Path | str,
                       *,
                       remove_rules: bool = False) -> SchemaWorkspace:
    """Patch an exported Schema doc by re-linking attribute rules to table names

    Args:
        schema (Path|str): The input schema to patch
        remove_rules (bool): Remove attribute rules from the schema (default: False)

    Returns:
        SchemaWorkspace: A patched schema dictionary
    """
    # Load schema
    if isinstance(schema, dict):
        workspace = schema
    else:
        schema = Path(schema)
        if not schema.suffix == '.json':
            raise ValueError(f'Schema Patching can only be done on json schemas!, got {schema.suffix}')
        workspace: SchemaWorkspace = json.load(schema.open(encoding='utf-8'))

    # Get all root Features
    features = [
        ds
        for ds in workspace['datasets']
        if 'datasets' not in ds
    ]
    # Get all features that live in a FeatureDataset
    # Use listcomp since typing is weird
    [
        features.extend(ds['datasets'])
        for ds in workspace['datasets']
        if 'datasets' in ds
    ]
    # Get a translation dictionary of catalogID -> name
    guid_to_name = {
        fc['catalogID']: fc['name']
        for fc in features
        if 'catalogID' in fc
    }
    for feature in filter(lambda fc: 'attributeRules' in fc, features):
        feature: SchemaDataset
        if remove_rules:
            feature['attributeRules'] = []
            continue
        rules = feature['attributeRules']
        for rule in rules:
            script = rule['scriptExpression']
            for guid, name in guid_to_name.items():
                script = script.replace(guid, name)
            rule['scriptExpression'] = script
    return workspace

print

print(
    *values: object,
    sep: str = ' ',
    end: str = '\n',
    file: Any = None,
    flush: bool = False,
    severity: Literal['INFO', 'WARNING', 'ERROR']
    | None = None,
) -> None

Print a message to the ArcGIS Pro message queue and stdout set severity to 'WARNING' or 'ERROR' to print to the ArcGIS Pro message queue with the appropriate severity

Source code in src/arcpie/utils.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
def print(*values: object,
          sep: str = " ",
          end: str = "\n",
          file: Any = None,
          flush: bool = False,
          severity: Literal['INFO', 'WARNING', 'ERROR'] | None = None) -> None:
    """ Print a message to the ArcGIS Pro message queue and stdout
    set severity to 'WARNING' or 'ERROR' to print to the ArcGIS Pro message queue with the appropriate severity
    """

    # Print the message to stdout
    builtins.print(*values, sep=sep, end=end, file=file, flush=flush)

    end = "" if end == '\n' else end
    message = f"{sep.join(map(str, values))}{end}"
    # Print the message to the ArcGIS Pro message queue with the appropriate severity
    match severity:
        case "WARNING":
            AddWarning(f"{message}")
        case "ERROR":
            AddError(f"{message}")
        case _:
            AddMessage(f"{message}")
    return

shortest_path

shortest_path(
    source: PointLike,
    target: PointLike,
    network: LineCollection,
    *,
    all_paths: Literal[False] = False,
    method: Literal[
        'dijkstra', 'bellman-ford'
    ] = 'dijkstra',
    weighted: bool = True,
    precision: int = 6,
) -> Polyline | None
shortest_path(
    source: PointLike,
    target: PointLike,
    network: LineCollection,
    *,
    all_paths: Literal[True] = True,
    method: Literal[
        'dijkstra', 'bellman-ford'
    ] = 'dijkstra',
    weighted: bool = True,
    precision: int = 6,
) -> list[Polyline] | None
shortest_path(
    source: PointLike,
    target: PointLike,
    network: LineCollection,
    *,
    all_paths: bool = False,
    method: Literal[
        'dijkstra', 'bellman-ford'
    ] = 'dijkstra',
    weighted: bool = True,
    precision: int = 6,
) -> Polyline | list[Polyline] | None

Find the shortest path or paths given a source point, target point and network of Polylines

PARAMETER DESCRIPTION

source

The start point for the path

TYPE: PointGeometry | Point

target

The end point for the path

TYPE: PointGeometry | Point

network

The polylines to traverse

TYPE: FeatureClass[Polyline, Any] | Sequence[Polyline] | Iterator[Polyline]

all_paths

If True, yield all shortest paths from (default: False)

TYPE: bool DEFAULT: False

method

The graph traversal algorithm to use (default: 'dijkstra')

TYPE: Literal['dijkstra', 'bellman-ford'] DEFAULT: 'dijkstra'

weighted

Use line lengths to weight the paths (default: True)

TYPE: bool DEFAULT: True

precision

Number of decimal places to round coordinates to (default: 6)

TYPE: int DEFAULT: 6

RETURNS DESCRIPTION
Polyline | None

The unioned polyline of the path or None if no path is found

YIELDS DESCRIPTION
Polyline

Yields all shortest paths if all_paths is set (None is still returned if no path found)

RAISES DESCRIPTION
ValueError

When input arguments are not of the correct types (PointLike, PointLike, LineCollection)

Example
path = shortest_path(p1, p2, line_features)
paths = shortest_path(p1, p2, line_features, all_paths=True)

if path is None:
    print('No Path')
else:
    print(path.length)

# Check that path(s) were found
if paths is None:
    print('No Path')
else:
    for p in paths:
        print(p.length)
Source code in src/arcpie/utils.py
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
def shortest_path(
    source: PointLike,
    target: PointLike,
    network: LineCollection,
    *,
    all_paths: bool = False,
    method: Literal['dijkstra', 'bellman-ford'] = 'dijkstra',
    weighted: bool = True,
    precision: int = 6,
    ) -> Polyline | list[Polyline] | None:
    """Find the shortest path or paths given a source point, target point and network of Polylines

    Args:
        source (PointGeometry | Point): The start point for the path
        target (PointGeometry | Point): The end point for the path
        network (FeatureClass[Polyline, Any] | Sequence[Polyline] | Iterator[Polyline]): The polylines to traverse
        all_paths (bool): If True, yield all shortest paths from (default: False)
        method (Literal['dijkstra', 'bellman-ford']): The graph traversal algorithm to use (default: 'dijkstra')
        weighted (bool): Use line lengths to weight the paths (default: True)
        precision (int): Number of decimal places to round coordinates to (default: 6)

    Returns:
        (Polyline | None): The unioned polyline of the path or None if no path is found

    Yields:
        (Polyline): Yields all shortest paths if `all_paths` is set (None is still *returned* if no path found)

    Raises:
        (ValueError): When input arguments are not of the correct types (`PointLike`, `PointLike`, `LineCollection`)

    Example:
        ```python
        path = shortest_path(p1, p2, line_features)
        paths = shortest_path(p1, p2, line_features, all_paths=True)

        if path is None:
            print('No Path')
        else:
            print(path.length)

        # Check that path(s) were found
        if paths is None:
            print('No Path')
        else:
            for p in paths:
                print(p.length)
        ```
    """

    # Parameter Validations
    if isinstance(network, FeatureClass):
        if network.describe.shapeType != 'Polyline':
            raise ValueError('network must have polyline geometry')
        network = list(network.shapes)

    if not isinstance(network, Sequence):
        network = list(network)

    if not network:
        return None

    if not isinstance(network[0], Polyline):  # type: ignore
        raise ValueError('network must have polyline geometry')

    # Project point geometries to match reference of network, extract X,Y tuples
    if isinstance(source, PointGeometry):
        if source.isMultipart:
            raise ValueError('source point must not be multipart')
        source = source.projectAs(network[0].spatialReference).centroid
    source_pt = (round(source.X, precision), round(source.Y, precision))

    if isinstance(target, PointGeometry):
        if target.isMultipart:
            raise ValueError('target point must not be multipart')
        target = target.projectAs(network[0].spatialReference).centroid
    target_pt = (round(target.X, precision), round(target.Y, precision))

    G = Graph(  # type: ignore
        [
            (
                (round(line.firstPoint.X, precision), round(line.firstPoint.Y, precision)),
                (round(line.lastPoint.X, precision), round(line.lastPoint.Y, precision)),
                {'shape': line, 'length': line.length}
            )
            for line in network
        ]  # type: ignore
    )
    try:
        if all_paths:
            paths = nx_all_shortest_paths(G, source_pt, target_pt, weight='length' if weighted else None, method=method)  # type: ignore
        else:
            paths = nx_shortest_path(G, source_pt, target_pt, weight='length' if weighted else None, method=method)  # type: ignore
    except (NodeNotFound, NetworkXNoPath):
        return None

    if all_paths:
        new_paths: list[Polyline] = []
        for path in paths:
            assert isinstance(path, list)
            edges: list[Polyline] = [G.get_edge_data(u, v)['shape'] for u, v in itertools.pairwise(path)]
            new_paths.append(reduce(lambda acc, s: acc.union(s), edges))  # pyright: ignore[reportArgumentType]
        return new_paths

    else:
        assert isinstance(paths, list)
        edges: list[Polyline] = [G.get_edge_data(u, v)['shape'] for u, v in itertools.pairwise(paths)]
        if edges:
            return reduce(lambda acc, s: acc.union(s), edges)  # pyright: ignore[reportReturnType]

split_at_points

split_at_points(
    lines: FeatureClass[Polyline, Any],
    points: FeatureClass[PointGeometry, Any],
    *,
    buffer: float = 0.0,
    min_len: float = 0.0,
) -> Iterator[tuple[int, Polyline]]

Split lines at provided points

PARAMETER DESCRIPTION

lines

Line features to split

TYPE: FeatureClass[Polyline]

points

Points to split on

TYPE: FeatureClass[PointGeometry]

buffer

Split buffer in feature units (default: 0.0 [exact])

TYPE: float DEFAULT: 0.0

min_len

Minumum length for a new line in feature units (default: 0.0)

TYPE: float DEFAULT: 0.0

YIELDS DESCRIPTION
tuple[int, Polyline]]

Tuples of parent OID and child shape

Warning

When splitting features in differing projections, the point features will be projected into the spatial reference of the line features.

Example
>>> # Simple process for splitting lines in place
>>> # Initialize a set to capture the removed ids
>>> removed: set[int] = set()
>>> with lines.editor:
...     # Insert new lines
...     with lines.insert_cursor('SHAPE@') as cur:
...         for parent, new_line in split_at_points(lines, points):
...             cur.insertRow([new_line])
...             removed.add(parent) # Add parent ID to removed
...     # Remove old lines (if you're inserting to the same featureclass)
...     with lines.update_cursor('OID@') as cur:
...         for _ in filter(lambda r: r[0] in removed, cur):
...             cur.deleteRow()
Source code in src/arcpie/utils.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
def split_at_points(lines: FeatureClass[Polyline, Any], points: FeatureClass[PointGeometry, Any],
                *,
                buffer: float = 0.0,
                min_len: float = 0.0) -> Iterator[tuple[int, Polyline]]:
    """Split lines at provided points

    Args:
        lines (FeatureClass[Polyline]): Line features to split
        points (FeatureClass[PointGeometry]): Points to split on
        buffer (float): Split buffer in feature units (default: 0.0 [exact])
        min_len (float): Minumum length for a new line in feature units (default: 0.0)

    Yields:
        ( tuple[int, Polyline]] ): Tuples of parent OID and child shape

    Warning:
        When splitting features in differing projections, the point features will be projected
        into the spatial reference of the line features.

    Example:
        ```python
        >>> # Simple process for splitting lines in place
        >>> # Initialize a set to capture the removed ids
        >>> removed: set[int] = set()
        >>> with lines.editor:
        ...     # Insert new lines
        ...     with lines.insert_cursor('SHAPE@') as cur:
        ...         for parent, new_line in split_at_points(lines, points):
        ...             cur.insertRow([new_line])
        ...             removed.add(parent) # Add parent ID to removed
        ...     # Remove old lines (if you're inserting to the same featureclass)
        ...     with lines.update_cursor('OID@') as cur:
        ...         for _ in filter(lambda r: r[0] in removed, cur):
        ...             cur.deleteRow()
        ```
    """
    line_iter: Iterator[tuple[Polyline, int]] = lines['SHAPE@', 'OID@']
    for line, oid in line_iter:
        int_points: list[PointGeometry] = []
        with points.reference_as(line.spatialReference), points.fields_as('SHAPE@'):
            int_points = [r['SHAPE@'] for r in points[line.buffer(buffer)]]

        if len(int_points) == 0 or all(p.touches(line) for p in int_points):
            continue

        prev_measure = 0.0
        measures = [*sorted(line.measureOnLine(p) for p in int_points), line.length]
        for measure in measures:
            seg = line.segmentAlongLine(prev_measure, measure)
            prev_measure = measure
            if seg and seg.length >= (min_len or 0):
                yield oid, seg

split_lines_at_points

split_lines_at_points(
    lines: Polyline
    | Sequence[Polyline]
    | Iterator[Polyline],
    points: Sequence[PointGeometry]
    | Iterator[PointGeometry],
) -> Iterator[Polyline]

Split a Polyline or Sequence/Iterable of polylines at provided points

PARAMETER DESCRIPTION

lines

The line or lines to split

TYPE: Polyline | Sequence[Polyline] | Iterator[Polyline]

points

The points to split at

TYPE: Sequence[PointGeometry] | Iterator[PointGeometry]

YIELDS DESCRIPTION
Polyline

Segments of the polyline split at the input points

Source code in src/arcpie/utils.py
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
def split_lines_at_points(lines: Polyline | Sequence[Polyline] | Iterator[Polyline], points: Sequence[PointGeometry] | Iterator[PointGeometry]) -> Iterator[Polyline]:
    """Split a Polyline or Sequence/Iterable of polylines at provided points

    Args:
        lines (Polyline | Sequence[Polyline] | Iterator[Polyline]): The line or lines to split
        points (Sequence[PointGeometry] | Iterator[PointGeometry]): The points to split at

    Yields:
        (Polyline): Segments of the polyline split at the input points
    """
    if isinstance(lines, Polyline):
        lines = [lines]
    if not isinstance(points, list):
        points = list(points)

    for line in lines:
        int_points = [p for p in points if not p.disjoint(line)]
        if not int_points:
            yield line
            continue
        if all(p.touches(line) for p in int_points):
            yield line
            continue
        prev_measure = 0.0
        measures = sorted(line.measureOnLine(p) for p in int_points)
        for measure in [*measures, line.length]:
            if prev_measure == measure:
                continue
            yield line.segmentAlongLine(prev_measure, measure).projectAs(line.spatialReference)
            prev_measure = measure

two_point_circle

two_point_circle(
    center: Point | PointGeometry,
    end: Point | PointGeometry,
    ref: SpatialReference | None = None,
) -> Polyline

Create a circle using a center point and an end point

PARAMETER DESCRIPTION

center

The center of the circle

TYPE: Point | PointGeometry

end

(Point|PointGeometry): The end point of the arc (distance from center is Circle radius)

TYPE: Point | PointGeometry

ref

(SpatialReference|None): The SpatialReference to use with the returned geometry

TYPE: SpatialReference | None DEFAULT: None

RETURNS DESCRIPTION
Polyline

A Circular Polyline

Note

If PointGeometries are provided, they will be projected as the provided ref If no ref is provided, the shape will inherit the reference of the center

Reference resolution is as follows: ref -> center.spatialReference -> end.spatialReference

If both points are Point objects with no spatial reference, and no ref is provided, The returned Polyline will have no spatial reference

Source code in src/arcpie/utils.py
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
def two_point_circle(center: Point | PointGeometry, end: Point | PointGeometry, ref: SpatialReference | None = None) -> Polyline:
    """Create a circle using a center point and an end point

    Args:
        center (Point|PointGeometry): The center of the circle
        end: (Point|PointGeometry): The end point of the arc (distance from center is Circle radius)
        ref: (SpatialReference|None): The SpatialReference to use with the returned geometry

    Returns:
        (Polyline): A Circular Polyline

    Note:
        If PointGeometries are provided, they will be projected as the provided ref
         If no ref is provided, the shape will inherit the reference of the center

        Reference resolution is as follows:
        `ref -> center.spatialReference -> end.spatialReference`

        If both points are Point objects with no spatial reference, and no ref is provided,
         The returned Polyline will have no spatial reference
    """
    if isinstance(center, PointGeometry):
        c_ref = center.spatialReference
        if ref and c_ref != ref:
            center = center.projectAs(ref)
        else:
            ref = ref or c_ref
        center = center.centroid
    if isinstance(end, PointGeometry):
        e_ref = end.spatialReference
        if ref and e_ref != ref:
            end = end.projectAs(ref)
        else:
            ref = ref or e_ref
        end = end.centroid

    esri_json: dict[str, Any] = {}
    arc: dict[str, Any] = {'a': [[end.X, end.Y, end.Z, end.M], [center.X, center.Y], 0, 1]}
    esri_json['curvePaths'] = [[[end.X, end.Y, end.Z], arc]]
    if ref:
        esri_json['spatialReference'] = {'wkid': ref.factoryCode, 'latestWkid': ref.factoryCode}
    return AsShape(esri_json, esri_json=True)  # type: ignore

vector_at

vector_at(
    line: Polyline,
    point: PointGeometry | Point,
    *,
    delta: float = 0.01,
    snap: bool = False,
) -> Vector

Get the vector of the line at the given point (p-delta -> p+delta)

PARAMETER DESCRIPTION

line

The line to get a Vector for

TYPE: Polyline

point

The PointGeometry specifying the vector location (projects to line reference)

TYPE: PointGeometry | Point

delta

The distance (meters) to traverse the line in both directions (default: 0.01)

TYPE: float DEFAULT: 0.01

snap

If the input point is disjoint from the line, snap it to the line (default: 'False')

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
Vector

A Vector of length delta*2 that describes the slope of the line at the provided point

Source code in src/arcpie/utils.py
745
746
747
748
749
750
751
752
753
754
755
756
757
758
def vector_at(line: Polyline, point: PointGeometry | Point, *, delta: float = 0.01, snap: bool = False) -> Vector:
    """Get the vector of the line at the given point (p-delta -> p+delta)

    Args:
        line: The line to get a Vector for
        point: The PointGeometry specifying the vector location (projects to line reference)
        delta: The distance (meters) to traverse the line in both directions (default: `0.01`)
        snap: If the input point is disjoint from the line, snap it to the line (default: 'False')

    Returns:
        A Vector of length delta*2 that describes the slope of the line at the provided point
    """
    v1, v2 = vectors_at(line, point, delta=delta, snap=snap)
    return v1 + v2

vectors_at

vectors_at(
    line: Polyline,
    point: PointGeometry | Point,
    *,
    delta: float = 0.01,
    snap: bool = False,
) -> tuple[Vector, Vector]

Get the vector of the line at the given point (p-delta -> p+delta)

PARAMETER DESCRIPTION

line

The line to get a Vector for

TYPE: Polyline

point

The PointGeometry specifying the vector location (projects to line reference)

TYPE: PointGeometry | Point

delta

The distance (meters) to traverse the line (default: 0.01)

TYPE: float DEFAULT: 0.01

snap

If the input point is disjoint from the line, snap it to the line (default: 'False')

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
tuple[Vector, Vector]

A tuple of Vectors representing the bearing to start and end at distance delta from point

Note

If the in point is a start/end point on the line, one of the Vectors will be a null vector!

Source code in src/arcpie/utils.py
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
def vectors_at(line: Polyline, point: PointGeometry | Point, *, delta: float = 0.01, snap: bool = False) -> tuple[Vector, Vector]:
    """Get the vector of the line at the given point (p-delta -> p+delta)

    Args:
        line: The line to get a Vector for
        point: The PointGeometry specifying the vector location (projects to line reference)
        delta: The distance (meters) to traverse the line (default: `0.01`)
        snap: If the input point is disjoint from the line, snap it to the line (default: 'False')

    Returns:
        A tuple of Vectors representing the bearing to start and end at distance delta from point

    Raises:
        ValueError if the point is disjoint from the line and `snap` is unset

    Note:
        If the in point is a start/end point on the line, one of the Vectors will be a null vector!
    """
    ref = line.spatialReference
    mpu = ref.metersPerUnit
    if isinstance(point, Point):
        point = PointGeometry(point, ref)
    else:
        point.projectAs(ref)

    if line.disjoint(point):
        if snap:
            point = line.snapToLine(point)
        else:
            raise ValueError('Vector point must not be disjoint from line (use `snap=True` to snap point to line)')

    pt_meas = line.measureOnLine(point) * mpu

    # Handle firstPoint
    if pt_meas < delta:
        pt_meas = delta
        point = line.positionAlongLine(pt_meas)
    # Handle lastPoint
    elif pt_meas == line.length:
        pt_meas = line.length - delta
        point = line.positionAlongLine(pt_meas)

    plus = line.positionAlongLine((pt_meas + delta) / mpu)
    minus = line.positionAlongLine((pt_meas - delta) / mpu)
    return Vector(point, plus), Vector(point, minus)