Exemplo n.º 1
0
    def __init__(
        self,
        dataset_path: Union[Path, str],
        *,
        field_names: Optional[Iterable[str]] = None,
        dataset_where_sql: Optional[str] = None,
        copy_path: Optional[Union[Path, str]] = None,
        force_nonspatial: bool = False,
    ) -> None:
        """Initialize instance.

        Note:
            To make a temp dataset without copying any template rows:
            `dataset_where_sql="0 = 1"`

        Args:
            dataset_path: Path to original dataset.
            copy_path: Path to copy dataset. If set to None, path will be auto-
                generated.
            field_names: Collection of field names to include in copy. If set to None,
                all fields will be included.
            dataset_where_sql: SQL where-clause property for original dataset
                subselection.
            force_nonspatial: Forces view to be nonspatial if True.
        """
        self.copy_path = Path(copy_path) if copy_path else unique_path(
            "TempCopy")
        self.dataset = Dataset(path=dataset_path)
        self.dataset_path = Path(dataset_path)
        self.dataset_where_sql = dataset_where_sql
        self.field_names = (self.dataset.field_names
                            if field_names is None else list(field_names))
        self.is_spatial = self.dataset.is_spatial and not force_nonspatial
Exemplo n.º 2
0
 def available_transform_path(self) -> Path:
     """Path in transformation workspace available for use as dataset."""
     path = unique_path(prefix=self.slug + "_",
                        workspace_path=self.workspace_path)
     while dataset.is_valid(path):
         path = self.available_transform_path
     return path
Exemplo n.º 3
0
def adjacent_neighbors_map(
    dataset_path: Union[Path, str],
    *,
    id_field_names: Iterable[str],
    dataset_where_sql: Optional[str] = None,
    exclude_overlap: bool = False,
    include_corner: bool = False,
) -> Dict[Union[Tuple[Any], Any], Set[Union[Tuple[Any], Any]]]:
    """Return mapping of feature ID to set of adjacent feature IDs.

    Notes:
        Only works for polygon geometries.
        If id_field_names only has one field name, feature IDs in the mapping will be
            the single value of that field, not a tuple.

    Args:
        dataset_path: Path to dataset.
        id_field_names: Names of the feature ID fields.
        dataset_where_sql: SQL where-clause for dataset subselection.
        exclude_overlap: Exclude features that overlap, but do not have adjacent edges
            or nodes if True.
        include_corner: Include features that have adjacent corner nodes, but no
            adjacent edges if True.
    """
    dataset_path = Path(dataset_path)
    id_field_names = list(id_field_names)
    # Lowercase to avoid casing mismatch.
    # id_field_names = [name.lower() for name in id_field_names]
    view = DatasetView(dataset_path,
                       field_names=id_field_names,
                       dataset_where_sql=dataset_where_sql)
    with view:
        temp_neighbor_path = unique_path("neighbor")
        # ArcPy2.8.0: Convert Path to str.
        arcpy.analysis.PolygonNeighbors(
            in_features=view.name,
            out_table=str(temp_neighbor_path),
            in_fields=id_field_names,
            area_overlap=not exclude_overlap,
            both_sides=True,
        )
    adjacent_neighbors = {}
    for row in features.as_dicts(temp_neighbor_path):
        # Lowercase to avoid casing mismatch.
        row = {key.lower(): val for key, val in row.items()}
        if len(id_field_names) == 1:
            source_id = row[f"src_{id_field_names[0]}"]
            neighbor_id = row[f"nbr_{id_field_names[0]}"]
        else:
            source_id = tuple(row[f"src_{name}"] for name in id_field_names)
            neighbor_id = tuple(row[f"nbr_{name}"] for name in id_field_names)
        if source_id not in adjacent_neighbors:
            adjacent_neighbors[source_id] = set()
        if not include_corner and not row["length"] and not row["area"]:
            continue

        adjacent_neighbors[source_id].add(neighbor_id)
    dataset.delete(temp_neighbor_path)
    return adjacent_neighbors
Exemplo n.º 4
0
def nearest_features(
    dataset_path: Union[Path, str],
    *,
    id_field_name: str,
    near_path: Union[Path, str],
    near_id_field_name: str,
    dataset_where_sql: Optional[str] = None,
    near_where_sql: Optional[str] = None,
    max_distance: Optional[Union[float, int]] = None,
    near_rank: int = 1,
) -> Iterator[Dict[str, Any]]:
    """Generate info dictionaries for relationship with Nth-nearest near-feature.

    Args:
        dataset_path: Path to dataset.
        id_field_name: Name of dataset ID field.
        near_path: Path to near-dataset.
        near_id_field_name: Name of the near-dataset ID field.
        dataset_where_sql: SQL where-clause for dataset subselection.
        near_where_sql: SQL where-clause for near-dataset subselection.
        max_distance: Maximum distance to search for near-features, in units of the
            dataset.
        near_rank: Nearness rank of the feature to map info for (Nth-nearest).

    Yields:
        Nearest feature details.
        Keys:
            * dataset_id
            * near_id
            * angle: Angle from dataset feature & near-feature, in decimal degrees.
            * distance: Distance between feature & near-feature, in units of the
                dataset.
    """
    dataset_path = Path(dataset_path)
    near_path = Path(near_path)
    view = DatasetView(dataset_path, dataset_where_sql=dataset_where_sql)
    near_view = DatasetView(near_path, dataset_where_sql=near_where_sql)
    with view, near_view:
        temp_near_path = unique_path("near")
        # ArcPy2.8.0: Convert Path to str.
        arcpy.analysis.GenerateNearTable(
            in_features=view.name,
            near_features=near_view.name,
            out_table=str(temp_near_path),
            search_radius=max_distance,
            angle=True,
            closest=(near_rank == 1),
            closest_count=near_rank,
        )
        oid_id_map = dict(
            features.as_tuples(view.name, field_names=["OID@", id_field_name]))
        near_oid_id_map = dict(
            features.as_tuples(near_view.name,
                               field_names=["OID@", near_id_field_name]))
    _features = features.as_dicts(
        temp_near_path,
        field_names=["IN_FID", "NEAR_FID", "NEAR_ANGLE", "NEAR_DIST"],
        dataset_where_sql=f"NEAR_RANK = {near_rank}"
        if near_rank != 1 else None,
    )
    for feature in _features:
        yield {
            "dataset_id": oid_id_map[feature["IN_FID"]],
            "near_id": near_oid_id_map[feature["NEAR_FID"]],
            "angle": feature["NEAR_ANGLE"],
            "distance": feature["NEAR_DIST"],
        }

    dataset.delete(temp_near_path)
Exemplo n.º 5
0
def update_by_central_overlay(
    dataset_path: Union[Path, str],
    field_name: str,
    *,
    overlay_dataset_path: Union[Path, str],
    overlay_field_name: str,
    dataset_where_sql: Optional[str] = None,
    overlay_where_sql: Optional[str] = None,
    replacement_value: Optional[Any] = None,
    tolerance: Optional[float] = None,
    use_edit_session: bool = False,
    log_level: int = logging.INFO,
) -> Counter:
    """Update attribute values by finding the central overlay feature value.

    Notes:
        Since only one value will be selected in the overlay, operations with multiple
        overlaying features will respect the geoprocessing environment merge rule. This
        rule generally defaults to the value of the "first" feature.

    Args:
        dataset_path: Path to dataset.
        field_name: Name of field.
        overlay_dataset_path: Path to overlay-dataset.
        overlay_field_name: Name of overlay-field.
        dataset_where_sql: SQL where-clause for dataset subselection.
        overlay_where_sql: SQL where-clause for overlay-dataset subselection.
        replacement_value: Value to replace a present overlay-field value with. If set
            to None, no replacement will occur.
        tolerance: Tolerance for coincidence, in units of the dataset. If set to None,
            will use the default tolerance for the workspace of the dataset.
        use_edit_session: True if edits are to be made in an edit session.
        log_level: Level to log the function at.

    Returns:
        Attribute counts for each update-state.
    """
    dataset_path = Path(dataset_path)
    overlay_dataset_path = Path(overlay_dataset_path)
    LOG.log(
        log_level,
        "Start: Update attributes in `%s.%s` by central-overlay value in `%s.%s`.",
        dataset_path,
        field_name,
        overlay_dataset_path,
        overlay_field_name,
    )
    original_tolerance = arcpy.env.XYTolerance
    # Do *not* include any fields here (avoids name collisions in temporary output).
    view = DatasetView(dataset_path,
                       field_names=[],
                       dataset_where_sql=dataset_where_sql)
    overlay_view = DatasetView(
        overlay_dataset_path,
        field_names=[overlay_field_name],
        dataset_where_sql=overlay_where_sql,
    )
    with view, overlay_view:
        temp_output_path = unique_path("output")
        if tolerance is not None:
            arcpy.env.XYTolerance = tolerance
        arcpy.analysis.SpatialJoin(
            target_features=view.name,
            join_features=overlay_view.name,
            # ArcPy2.8.0: Convert to str.
            out_feature_class=str(temp_output_path),
            join_operation="JOIN_ONE_TO_ONE",
            join_type="KEEP_ALL",
            match_option="HAVE_THEIR_CENTER_IN",
        )
    arcpy.env.XYTolerance = original_tolerance
    if replacement_value is not None:
        update_by_function(
            temp_output_path,
            overlay_field_name,
            function=lambda x: replacement_value if x else None,
            log_level=logging.DEBUG,
        )
    states = update_by_joined_value(
        dataset_path,
        field_name,
        key_field_names=["OID@"],
        join_dataset_path=temp_output_path,
        join_field_name=overlay_field_name,
        join_key_field_names=["TARGET_FID"],
        dataset_where_sql=dataset_where_sql,
        use_edit_session=use_edit_session,
        log_level=logging.DEBUG,
    )
    # ArcPy2.8.0: Convert to str.
    arcpy.management.Delete(str(temp_output_path))
    log_entity_states("attributes", states, logger=LOG, log_level=log_level)
    LOG.log(log_level, "End: Update.")
    return states
Exemplo n.º 6
0
def update_by_overlay_count(
    dataset_path: Union[Path, str],
    field_name: str,
    *,
    overlay_dataset_path: Union[Path, str],
    dataset_where_sql: Optional[str] = None,
    overlay_where_sql: Optional[str] = None,
    tolerance: Optional[float] = None,
    use_edit_session: bool = False,
    log_level: int = logging.INFO,
) -> Counter:
    """Update attribute values by count of overlay features.

    Args:
        dataset_path: Path to dataset.
        field_name: Name of field.
        overlay_dataset_path: Path to overlay-dataset.

    Keyword Args:
        dataset_where_sql: SQL where-clause for dataset subselection.
        overlay_where_sql: SQL where-clause for overlay-dataset subselection.
        tolerance: Tolerance for coincidence, in units of the dataset. If set to None,
            will use the default tolerance for the workspace of the dataset.
        use_edit_session: True if edits are to be made in an edit session.
        log_level: Level to log the function at.

    Returns:
        Attribute counts for each update-state.

    Raises:
        RuntimeError: If attribute cannot be updated.
    """
    dataset_path = Path(dataset_path)
    overlay_dataset_path = Path(overlay_dataset_path)
    LOG.log(
        log_level,
        "Start: Update attributes in `%s.%s` by overlay feature counts from `%s`.",
        dataset_path,
        field_name,
        overlay_dataset_path,
    )
    original_tolerance = arcpy.env.XYTolerance
    view = DatasetView(dataset_path,
                       field_names=[],
                       dataset_where_sql=dataset_where_sql)
    overlay_view = DatasetView(
        overlay_dataset_path,
        field_names=[],
        dataset_where_sql=overlay_where_sql,
    )
    with view, overlay_view:
        if tolerance is not None:
            arcpy.env.XYTolerance = tolerance
        temp_output_path = unique_path("output")
        arcpy.analysis.SpatialJoin(
            target_features=view.name,
            join_features=overlay_view.name,
            # ArcPy2.8.0: Convert to str.
            out_feature_class=str(temp_output_path),
            join_operation="JOIN_ONE_TO_ONE",
            join_type="KEEP_COMMON",
            match_option="INTERSECT",
        )
    arcpy.env.XYTolerance = original_tolerance
    cursor = arcpy.da.SearchCursor(
        # ArcPy2.8.0: Convert to str.
        in_table=str(temp_output_path),
        field_names=["TARGET_FID", "Join_Count"],
    )
    with cursor:
        oid_overlay_count = dict(cursor)
    # ArcPy2.8.0: Convert to str.
    arcpy.management.Delete(str(temp_output_path))
    cursor = arcpy.da.UpdateCursor(
        # ArcPy2.8.0: Convert to str.
        in_table=str(dataset_path),
        field_names=["OID@", field_name],
        where_clause=dataset_where_sql,
    )
    session = Editing(Dataset(dataset_path).workspace_path, use_edit_session)
    states = Counter()
    with session, cursor:
        for feature in cursor:
            oid = feature[0]
            old_value = feature[1]
            new_value = oid_overlay_count.get(oid, 0)
            if same_value(old_value, new_value):
                states["unchanged"] += 1
            else:
                try:
                    cursor.updateRow([oid, new_value])
                    states["altered"] += 1
                except RuntimeError as error:
                    raise RuntimeError(
                        f"Update cursor failed: Offending value: `{new_value}`"
                    ) from error

    log_entity_states("attributes", states, logger=LOG, log_level=log_level)
    LOG.log(log_level, "End: Update.")
    return states
Exemplo n.º 7
0
def update_by_dominant_overlay(
    dataset_path: Union[Path, str],
    field_name: str,
    *,
    overlay_dataset_path: Union[Path, str],
    overlay_field_name: str,
    dataset_where_sql: Optional[str] = None,
    overlay_where_sql: Optional[str] = None,
    include_missing_area: bool = False,
    tolerance: Optional[float] = None,
    use_edit_session: bool = False,
    log_level: int = logging.INFO,
) -> Counter:
    """Update attribute values by finding the dominant overlay feature value.
    Args:
        dataset_path: Path to dataset.
        field_name: Name of field.
        overlay_dataset_path: Path to overlay-dataset.
        overlay_field_name: Name of overlay-field.
        dataset_where_sql: SQL where-clause for dataset subselection.
        overlay_where_sql: SQL where-clause for overlay-dataset subselection.
        include_missing_area: If True, the collective area where no
            overlay value exists (i.e. no overlay geometry + overlay of NoneType value)
            is considered a valid candidate for the dominant overlay.
        tolerance: Tolerance for coincidence, in units of the dataset. If set to None,
            will use the default tolerance for the workspace of the dataset.
        use_edit_session: True if edits are to be made in an edit session.
        log_level: Level to log the function at.

    Returns:
        Attribute counts for each update-state.
    """
    dataset_path = Path(dataset_path)
    overlay_dataset_path = Path(overlay_dataset_path)
    LOG.log(
        log_level,
        "Start: Update attributes in `%s.%s` by dominant overlay value in `%s.%s`.",
        dataset_path,
        field_name,
        overlay_dataset_path,
        overlay_field_name,
    )
    original_tolerance = arcpy.env.XYTolerance
    # Do *not* include any fields here (avoids name collisions in temporary output).
    view = DatasetView(dataset_path,
                       field_names=[],
                       dataset_where_sql=dataset_where_sql)
    overlay_view = DatasetView(
        overlay_dataset_path,
        field_names=[overlay_field_name],
        dataset_where_sql=overlay_where_sql,
    )
    with view, overlay_view:
        temp_output_path = unique_path("output")
        if tolerance is not None:
            arcpy.env.XYTolerance = tolerance
        arcpy.analysis.Identity(
            in_features=view.name,
            identity_features=overlay_view.name,
            # ArcPy2.8.0: Convert to str.
            out_feature_class=str(temp_output_path),
            join_attributes="ALL",
        )
    arcpy.env.XYTolerance = original_tolerance
    # Identity makes custom OID field names - in_features OID field comes first.
    oid_field_names = [
        field_name for field_name in Dataset(temp_output_path).field_names
        if field_name.startswith("FID_")
    ]
    oid_value_area = {}
    cursor = arcpy.da.SearchCursor(
        # ArcPy2.8.0: Convert to str.
        in_table=str(temp_output_path),
        field_names=oid_field_names + [overlay_field_name, "SHAPE@AREA"],
    )
    with cursor:
        for oid, overlay_oid, value, area in cursor:
            # Def check for -1 OID (no overlay feature): identity does not set to None.
            if overlay_oid == -1:
                value = None
            if value is None and not include_missing_area:
                continue

            if oid not in oid_value_area:
                oid_value_area[oid] = defaultdict(float)
            oid_value_area[oid][value] += area
    # ArcPy2.8.0: Convert to str.
    arcpy.management.Delete(str(temp_output_path))
    oid_dominant_value = {
        oid: max(value_area.items(), key=itemgetter(1))[0]
        for oid, value_area in oid_value_area.items()
    }
    states = update_by_mapping(
        dataset_path,
        field_name,
        mapping=oid_dominant_value,
        key_field_names=["OID@"],
        dataset_where_sql=dataset_where_sql,
        use_edit_session=use_edit_session,
        log_level=logging.DEBUG,
    )
    log_entity_states("attributes", states, logger=LOG, log_level=log_level)
    LOG.log(log_level, "End: Update.")
    return states