🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
263 lines
7.2 KiB
Markdown
263 lines
7.2 KiB
Markdown
# Info Objects - Metric Field Analysis
|
|
|
|
## Overview
|
|
|
|
The `metric` field in info objects (map-info-object) is **NOT** a simple integer or boolean. It's a **calculated string** containing measurements based on the geometry type.
|
|
|
|
## Data from Verofy API (Map 15685)
|
|
|
|
### Type 1: Marker (Point)
|
|
```json
|
|
{
|
|
"mapinfoobjecttypeId": 1,
|
|
"metric": "Lat: 40.760311 Lng: -124.172782"
|
|
}
|
|
```
|
|
**Format:** `"Lat: {latitude} Lng: {longitude}"`
|
|
|
|
---
|
|
|
|
### Type 2: Polyline (Lines/Cables)
|
|
```json
|
|
{
|
|
"mapinfoobjecttypeId": 2,
|
|
"metric": "Mileage: 0.1519; Footage: 802"
|
|
}
|
|
```
|
|
**Format:** `"Mileage: {miles}; Footage: {feet}"`
|
|
|
|
**Calculation:**
|
|
- Calculate the total length of the LineString
|
|
- Convert to miles and feet
|
|
- Format as shown
|
|
|
|
---
|
|
|
|
### Type 3: Polygon (Boundaries/Parcels)
|
|
```json
|
|
{
|
|
"mapinfoobjecttypeId": 3,
|
|
"metric": "Square Miles: 0.0065"
|
|
}
|
|
```
|
|
**Format:** `"Square Miles: {sq_miles}"`
|
|
|
|
**Calculation:**
|
|
- Calculate the area of the Polygon
|
|
- Convert to square miles
|
|
- Format as shown
|
|
|
|
---
|
|
|
|
## Required Implementation
|
|
|
|
### For Cables (Type 2 - Polyline)
|
|
|
|
Current code sends polygons/parcels to info objects. For cables specifically:
|
|
|
|
```python
|
|
from shapely.geometry import LineString
|
|
import geopandas as gpd
|
|
|
|
def calculate_line_metric(geometry):
|
|
"""Calculate metric string for a line geometry"""
|
|
if geometry.geom_type != 'LineString':
|
|
return "Invalid geometry type"
|
|
|
|
# Calculate length in degrees (WGS84)
|
|
# Convert to approximate miles and feet
|
|
# Note: This is approximate - for accurate results, reproject to appropriate CRS
|
|
|
|
# Rough conversion: 1 degree ≈ 69 miles at equator
|
|
# For more accuracy, use geopy or pyproj
|
|
|
|
length_degrees = geometry.length
|
|
length_miles = length_degrees * 69 # Approximate
|
|
length_feet = length_miles * 5280
|
|
|
|
return f"Mileage: {length_miles:.4f}; Footage: {length_feet:.0f}"
|
|
|
|
# In _upload_cables():
|
|
metric = calculate_line_metric(row.geometry)
|
|
info_data = {
|
|
"mapProjectId": int(map_id),
|
|
"name": str(row.get('Name', f'Cable-{idx}')),
|
|
"mapinfoobjecttypeId": 2,
|
|
"data": data,
|
|
"color": "#ffffff",
|
|
"alpha": "1.00",
|
|
"metric": metric # ✅ Calculated value
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### For Cabinet Boundaries & Parcels (Type 3 - Polygon)
|
|
|
|
```python
|
|
from shapely.geometry import Polygon
|
|
|
|
def calculate_polygon_metric(geometry):
|
|
"""Calculate metric string for a polygon geometry"""
|
|
if geometry.geom_type != 'Polygon':
|
|
return "Invalid geometry type"
|
|
|
|
# Calculate area in square degrees (WGS84)
|
|
# Convert to square miles
|
|
# Note: This is approximate - for accurate results, reproject to appropriate CRS
|
|
|
|
area_sq_degrees = geometry.area
|
|
# Rough conversion: 1 degree² ≈ 4,761 square miles at equator
|
|
# This varies by latitude, so this is very approximate
|
|
area_sq_miles = area_sq_degrees * 4761
|
|
|
|
return f"Square Miles: {area_sq_miles:.4f}"
|
|
|
|
# In _upload_cabinet_boundaries() and _upload_parcels():
|
|
metric = calculate_polygon_metric(row.geometry)
|
|
info_data = {
|
|
"mapProjectId": int(map_id),
|
|
"name": str(row.get('Name', f'Boundary-{idx}')),
|
|
"mapinfoobjecttypeId": 3,
|
|
"data": data,
|
|
"color": "#ffffff",
|
|
"alpha": "0.40",
|
|
"metric": metric # ✅ Calculated value
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Better Implementation Using GeoPandas
|
|
|
|
GeoPandas can handle CRS transformations for more accurate calculations:
|
|
|
|
```python
|
|
def calculate_geometry_metric(geometry, geom_type):
|
|
"""
|
|
Calculate metric string for any geometry type
|
|
|
|
Args:
|
|
geometry: Shapely geometry object
|
|
geom_type: 1 (Point), 2 (LineString), 3 (Polygon)
|
|
"""
|
|
if geom_type == 1: # Point/Marker
|
|
return f"Lat: {geometry.y:.6f} Lng: {geometry.x:.6f}"
|
|
|
|
elif geom_type == 2: # LineString/Cable
|
|
# For accurate length, project to UTM or other metric CRS
|
|
# Rough approximation using great circle distance
|
|
from shapely.ops import transform
|
|
import pyproj
|
|
from functools import partial
|
|
|
|
# Define projection from WGS84 to a metric system (meters)
|
|
project = partial(
|
|
pyproj.transform,
|
|
pyproj.Proj('EPSG:4326'), # WGS84
|
|
pyproj.Proj('EPSG:3857') # Web Mercator (meters)
|
|
)
|
|
|
|
# Transform and calculate length
|
|
line_projected = transform(project, geometry)
|
|
length_meters = line_projected.length
|
|
length_miles = length_meters * 0.000621371
|
|
length_feet = length_meters * 3.28084
|
|
|
|
return f"Mileage: {length_miles:.4f}; Footage: {length_feet:.0f}"
|
|
|
|
elif geom_type == 3: # Polygon
|
|
# Similar projection for area
|
|
from shapely.ops import transform
|
|
import pyproj
|
|
from functools import partial
|
|
|
|
project = partial(
|
|
pyproj.transform,
|
|
pyproj.Proj('EPSG:4326'), # WGS84
|
|
pyproj.Proj('EPSG:3857') # Web Mercator (meters)
|
|
)
|
|
|
|
polygon_projected = transform(project, geometry)
|
|
area_sq_meters = polygon_projected.area
|
|
area_sq_miles = area_sq_meters * 0.000000386102
|
|
|
|
return f"Square Miles: {area_sq_miles:.4f}"
|
|
|
|
return "Unknown type"
|
|
```
|
|
|
|
---
|
|
|
|
## Simplified Approach (Good Enough for Most Cases)
|
|
|
|
Since all geometries are in WGS84 (EPSG:4326), and the study area is around Eureka, CA (latitude ~40°):
|
|
|
|
```python
|
|
def calculate_metric_simple(geometry, geom_type):
|
|
"""
|
|
Simplified metric calculation
|
|
Good enough approximation for small areas at mid-latitudes
|
|
"""
|
|
if geom_type == 1: # Point
|
|
return f"Lat: {geometry.y:.6f} Lng: {geometry.x:.6f}"
|
|
|
|
elif geom_type == 2: # LineString
|
|
# At 40° latitude: 1 degree longitude ≈ 53 miles
|
|
# 1 degree latitude ≈ 69 miles
|
|
coords = list(geometry.coords)
|
|
total_miles = 0
|
|
|
|
for i in range(len(coords) - 1):
|
|
lon1, lat1 = coords[i]
|
|
lon2, lat2 = coords[i + 1]
|
|
|
|
# Approximate distance
|
|
dlat = (lat2 - lat1) * 69
|
|
dlon = (lon2 - lon1) * 53 # At 40° latitude
|
|
segment_miles = (dlat**2 + dlon**2)**0.5
|
|
total_miles += segment_miles
|
|
|
|
total_feet = total_miles * 5280
|
|
return f"Mileage: {total_miles:.4f}; Footage: {total_feet:.0f}"
|
|
|
|
elif geom_type == 3: # Polygon
|
|
# Approximate area calculation
|
|
# At 40° latitude: 1 deg² ≈ 3,657 square miles
|
|
area_sq_degrees = geometry.area
|
|
area_sq_miles = area_sq_degrees * 3657
|
|
return f"Square Miles: {area_sq_miles:.4f}"
|
|
|
|
return ""
|
|
```
|
|
|
|
---
|
|
|
|
## Recommendation
|
|
|
|
**Use GeoPandas with proper CRS transformation** for accurate results:
|
|
|
|
1. Read geometries (already in WGS84 / EPSG:4326)
|
|
2. Convert to a local projected CRS (like UTM Zone 10N - EPSG:32610 for California)
|
|
3. Calculate length/area in meters
|
|
4. Convert to miles/feet/square miles
|
|
5. Format as string
|
|
|
|
This ensures accurate measurements regardless of latitude.
|
|
|
|
---
|
|
|
|
## Impact on Current Code
|
|
|
|
**Current assumption:** `"metric": 0` ❌
|
|
|
|
**Reality:** `"metric": "Mileage: 0.1519; Footage: 802"` ✅
|
|
|
|
**Changes needed:**
|
|
1. Add metric calculation functions
|
|
2. Update `_upload_cables()` to calculate line metrics
|
|
3. Update `_upload_cabinet_boundaries()` to calculate polygon metrics
|
|
4. Update `_upload_parcels()` to calculate polygon metrics
|
|
|
|
This is **more complex than originally thought** but necessary for API compatibility.
|