2025-07-18 01:28:21 +00:00
import Foundation
import MapKit
2025-07-22 00:48:50 +00:00
import SwiftUI
import CoreLocation
import OSLog
2025-07-18 01:28:21 +00:00
2025-07-22 00:48:50 +00:00
// MARK: - R a w G e o J S O N S u p p o r t O n l y
2025-07-18 01:28:21 +00:00
struct GeoJSONFeatureCollection : Codable {
2025-07-23 20:58:46 +00:00
let type : String // A l w a y s " F e a t u r e C o l l e c t i o n "
let features : [ GeoJSONFeature ]
2025-07-18 01:28:21 +00:00
}
struct GeoJSONFeature : Codable {
2025-07-23 20:58:46 +00:00
let type : String // A l w a y s " F e a t u r e "
let id : Int ?
let geometry : GeoJSONGeometry
let properties : [ String : AnyCodableValue ] ?
// MARK: - G e o J S O N S t y l i n g P r o p e r t i e s
// / E x t r a c t f e a t u r e n a m e f r o m p r o p e r t i e s , d e f a u l t i n g t o e m p t y s t r i n g
var name : String {
// C h e c k f o r " N A M E " f i r s t ( u p p e r c a s e ) , t h e n " n a m e " ( l o w e r c a s e )
if case . string ( let value ) = properties ? [ " NAME " ] {
return value
}
if case . string ( let value ) = properties ? [ " name " ] {
return value
}
return " "
}
// / E x t r a c t l a y e r m e t a d a t a f r o m p r o p e r t i e s
var layerId : String ? {
if case . string ( let value ) = properties ? [ " layer_id " ] {
return value
}
return nil
}
var layerName : String ? {
if case . string ( let value ) = properties ? [ " layer_name " ] {
return value
}
return nil
}
var layerDescription : String ? {
if case . string ( let value ) = properties ? [ " description " ] {
return value
}
return nil
}
var isVisible : Bool {
if case . bool ( let value ) = properties ? [ " visible " ] {
return value
}
return true // D e f a u l t t o v i s i b l e
}
// MARK: - P o i n t / M a r k e r S t y l i n g
var markerColor : String ? {
if case . string ( let value ) = properties ? [ " marker-color " ] {
return value
}
return nil
}
var markerSize : String ? {
if case . string ( let value ) = properties ? [ " marker-size " ] {
return value
}
return " medium " // D e f a u l t s i z e
}
var markerSymbol : String ? {
if case . string ( let value ) = properties ? [ " marker-symbol " ] {
return value
}
return nil
}
// MARK: - S t r o k e / L i n e S t y l i n g
var strokeColor : String ? {
if case . string ( let value ) = properties ? [ " stroke " ] {
return value
}
return nil
}
var strokeWidth : Double {
if case . double ( let value ) = properties ? [ " stroke-width " ] {
return value
} else if case . int ( let value ) = properties ? [ " stroke-width " ] {
return Double ( value )
}
return 1.0 // D e f a u l t w i d t h
}
var strokeOpacity : Double {
if case . double ( let value ) = properties ? [ " stroke-opacity " ] {
return value
} else if case . int ( let value ) = properties ? [ " stroke-opacity " ] {
return Double ( value )
}
return 1.0 // D e f a u l t o p a c i t y
}
var lineDashArray : [ Double ] ? {
if case . array ( let values ) = properties ? [ " line-dasharray " ] {
return values . compactMap { value in
switch value {
case . double ( let d ) : return d
case . int ( let i ) : return Double ( i )
default : return nil
}
}
}
return nil
}
// MARK: - F i l l S t y l i n g
var fillColor : String ? {
if case . string ( let value ) = properties ? [ " fill " ] {
return value
}
return nil
}
var fillOpacity : Double {
if case . double ( let value ) = properties ? [ " fill-opacity " ] {
return value
} else if case . int ( let value ) = properties ? [ " fill-opacity " ] {
return Double ( value )
}
return 0.0 // D e f a u l t t o n o f i l l
}
// MARK: - C o m p u t e d R e n d e r i n g P r o p e r t i e s
// / G e t e f f e c t i v e s t r o k e c o l o r ( f a l l b a c k t o m a r k e r c o l o r f o r p o i n t s )
var effectiveStrokeColor : String {
return strokeColor ? ? markerColor ? ? " #000000 "
}
// / G e t e f f e c t i v e f i l l c o l o r ( f a l l b a c k t o s t r o k e c o l o r i f f i l l o p a c i t y > 0 )
var effectiveFillColor : String {
if fillOpacity > 0 {
return fillColor ? ? effectiveStrokeColor
}
return " #000000 "
}
// / C o n v e r t m a r k e r s i z e t o p o i n t r a d i u s
var markerRadius : CGFloat {
switch markerSize {
case " small " : return 4.0
case " medium " : return 8.0
case " large " : return 12.0
default : return 4.0
}
}
2025-07-22 00:48:50 +00:00
}
// MARK: - S t y l e d F e a t u r e W r a p p e r
// / W r a p p e r f o r a G e o J S O N f e a t u r e w i t h i t s s t y l i n g p r o p e r t i e s a n d m e t a d a t a
struct GeoJSONStyledFeature : Identifiable {
2025-07-23 20:58:46 +00:00
let id = UUID ( )
let feature : GeoJSONFeature
let overlayId : String
2026-04-18 09:28:33 -07:00
// / M K O v e r l a y p r e - c o m p u t e d o n c e a t i n i t — a v o i d s r e p e a t e d J S O N S e r i a l i z a t i o n + M K G e o J S O N D e c o d e r
// / c a l l s o n e v e r y m a p r e n d e r p a s s .
let precomputedOverlay : MKOverlay ?
2025-07-23 20:58:46 +00:00
2026-04-18 09:28:33 -07:00
init ( feature : GeoJSONFeature , overlayId : String ) {
self . feature = feature
self . overlayId = overlayId
// C a l l t h e s t a t i c h e l p e r a f t e r a l l s t o r e d p r o p e r t i e s a r e a s s i g n e d s o ` s e l f ` i s a v a i l a b l e
// f o r t h e i n s t a n c e — b u t w e d o n ' t a c t u a l l y n e e d s e l f h e r e , s o t h i s i s s a f e .
self . precomputedOverlay = GeoJSONStyledFeature . makeOverlay ( for : feature )
}
// / B u i l d s a n M K O v e r l a y f r o m a G e o J S O N f e a t u r e . S t a t i c s o i t c a n b e c a l l e d f r o m i n i t .
private static func makeOverlay ( for feature : GeoJSONFeature ) -> MKOverlay ? {
2025-07-28 11:30:03 -07:00
let featureDict : [ String : Any ] = [
" type " : feature . type ,
" geometry " : [
" type " : feature . geometry . type ,
" coordinates " : feature . geometry . coordinates . toAnyObject ( )
] ,
" properties " : feature . properties ? . mapValues { $0 . toAnyObject ( ) } ? ? [ : ]
]
2025-07-23 20:58:46 +00:00
2025-07-28 11:30:03 -07:00
do {
2025-07-23 20:58:46 +00:00
let geojsonData = try JSONSerialization . data ( withJSONObject : featureDict )
2026-04-18 09:28:33 -07:00
let mkFeatures = try MKGeoJSONDecoder ( ) . decode ( geojsonData )
if let mkFeature = mkFeatures . first as ? MKGeoJSONFeature ,
let geometry = mkFeature . geometry . first as ? MKOverlay {
return geometry
} else {
Logger . services . error ( " 🗺️ GeoJSONStyledFeature: Failed to create overlay - no valid MKOverlay geometry. " )
2025-07-23 20:58:46 +00:00
}
} catch {
2026-04-18 09:28:33 -07:00
Logger . services . error ( " 🗺️ GeoJSONStyledFeature: Failed to build overlay: \( error . localizedDescription ) " )
2025-07-23 20:58:46 +00:00
}
return nil
}
2026-04-18 09:28:33 -07:00
// / R e t u r n s t h e p r e - c o m p u t e d o v e r l a y . R e t a i n e d f o r A P I c o m p a t i b i l i t y .
func createOverlay ( ) -> MKOverlay ? { precomputedOverlay }
2025-07-23 20:58:46 +00:00
// / G e t s t r o k e s t y l e f o r t h i s f e a t u r e
var strokeStyle : StrokeStyle {
let dashArray = feature . lineDashArray
if let dashArray = dashArray , ! dashArray . isEmpty {
return StrokeStyle (
lineWidth : feature . strokeWidth ,
lineCap : . round ,
lineJoin : . round ,
dash : dashArray . map { CGFloat ( $0 ) }
)
} else {
return StrokeStyle (
lineWidth : feature . strokeWidth ,
lineCap : . round ,
lineJoin : . round
)
}
}
// / G e t s t r o k e c o l o r w i t h o p a c i t y
var strokeColor : Color {
return Color ( hex : feature . effectiveStrokeColor ) . opacity ( feature . strokeOpacity )
}
// / G e t f i l l c o l o r w i t h o p a c i t y
var fillColor : Color {
return Color ( hex : feature . effectiveFillColor ) . opacity ( feature . fillOpacity )
}
2025-07-18 01:28:21 +00:00
}
struct GeoJSONGeometry : Codable {
2025-07-23 20:58:46 +00:00
let type : String // " P o i n t " , " L i n e S t r i n g " , " P o l y g o n " , e t c .
let coordinates : AnyCodableValue // F l e x i b l e c o o r d i n a t e s t r u c t u r e
2025-07-18 01:28:21 +00:00
}
// MARK: - F l e x i b l e J S O N V a l u e T y p e
enum AnyCodableValue : Codable {
2025-07-23 20:58:46 +00:00
case string ( String )
case int ( Int )
case double ( Double )
case bool ( Bool )
case array ( [ AnyCodableValue ] )
case object ( [ String : AnyCodableValue ] )
case null
init ( from decoder : Decoder ) throws {
let container = try decoder . singleValueContainer ( )
if container . decodeNil ( ) {
self = . null
} else if let value = try ? container . decode ( Bool . self ) {
self = . bool ( value )
} else if let value = try ? container . decode ( Int . self ) {
self = . int ( value )
} else if let value = try ? container . decode ( Double . self ) {
self = . double ( value )
} else if let value = try ? container . decode ( String . self ) {
self = . string ( value )
} else if let value = try ? container . decode ( [ AnyCodableValue ] . self ) {
self = . array ( value )
} else if let value = try ? container . decode ( [ String : AnyCodableValue ] . self ) {
self = . object ( value )
} else {
throw DecodingError . typeMismatch ( AnyCodableValue . self , DecodingError . Context ( codingPath : decoder . codingPath , debugDescription : " Unable to decode AnyCodableValue " ) )
}
}
func encode ( to encoder : Encoder ) throws {
var container = encoder . singleValueContainer ( )
switch self {
case . null :
try container . encodeNil ( )
case . bool ( let value ) :
try container . encode ( value )
case . int ( let value ) :
try container . encode ( value )
case . double ( let value ) :
try container . encode ( value )
case . string ( let value ) :
try container . encode ( value )
case . array ( let value ) :
try container . encode ( value )
case . object ( let value ) :
try container . encode ( value )
}
}
// H e l p e r t o c o n v e r t c o o r d i n a t e s t o t h e f o r m a t e x p e c t e d b y M K G e o J S O N D e c o d e r
func toAnyObject ( ) -> Any {
switch self {
case . null :
return NSNull ( )
case . bool ( let value ) :
return value
case . int ( let value ) :
return value
case . double ( let value ) :
return value
case . string ( let value ) :
return value
case . array ( let values ) :
return values . map { $0 . toAnyObject ( ) }
case . object ( let dict ) :
return dict . mapValues { $0 . toAnyObject ( ) }
}
}
// H e l p e r t o c o n v e r t P o i n t c o o r d i n a t e s t o C L L o c a t i o n C o o r d i n a t e 2 D
func toCoordinate ( ) -> CLLocationCoordinate2D ? {
if case . array ( let coords ) = self ,
coords . count >= 2 {
let lon : Double
let lat : Double
switch coords [ 0 ] {
case . double ( let d ) : lon = d
case . int ( let i ) : lon = Double ( i )
default : return nil
}
switch coords [ 1 ] {
case . double ( let d ) : lat = d
case . int ( let i ) : lat = Double ( i )
default : return nil
}
return CLLocationCoordinate2D ( latitude : lat , longitude : lon )
}
return nil
}
2025-07-22 06:40:43 +00:00
}