GEOSwift / GEOSwift

The Swift Geometry Engine.
MIT License
1.45k stars 172 forks source link

Searching for point within a large geoJSON file #263

Open Jessers1 opened 1 year ago

Jessers1 commented 1 year ago

Is there an efficient built in method for finding which features within a geojson file contains a point without resorting to inefficient linear search? I've got a very large file that I'm having to linear search every location update and it's inducing heaps of lag.

func findSpeedLimit(location: CLLocationCoordinate2D, geoJSON: GeoJSON) -> Int? {
    // Decode the GeoJSON data into a FeatureCollection object
    guard case let .featureCollection(featureCollection) = geoJSON else {
        print("Error: GeoJSON data is not a FeatureCollection")
        return nil
    }

    // Create a Point object from the location coordinates
    let point = Point(x: location.longitude, y: location.latitude)
    //print("Looking for speed limit at location: \(location.latitude), \(location.longitude)")

    // Loop through the features in the feature collection
    for (index, feature) in featureCollection.features.enumerated() {
        // Check if the feature has a geometry and properties
        guard let geometry = feature.geometry,
              let properties = feature.properties else {
            print("Feature \(index) has no geometry or properties")
            continue
        }

        do {
            // Check if the geometry contains the point
            if try geometry.contains(point) {
                // Check if the properties have a speedLimitZoneValue key
                if let speedLimit = properties["speedLimitZoneValue"] {
                    let speedLimitString = String(describing: speedLimit)
                    if let number = Int(speedLimitString.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) {
                        //print (number)
                        //print("Speed limit found for feature \(index): \(number) km/h")
                        return number
                    }

                } else {
                    print("Feature \(index) has no 'speedLimitZoneValue' property")
                }
            } else {
                //print("Feature \(index) does not contain the point")
            }
        } catch {
            print("Error checking if geometry contains point: \(error)")
        }
    }

    return nil
}
macdrevx commented 1 year ago

The prepared geometry capability of GEOS would likely be a good fit for your use case. GEOSwift doesn't expose those APIs yet, but I'll consider this issue a feature request to add them!

macdrevx commented 1 year ago

Could you give https://github.com/GEOSwift/GEOSwift/pull/264 a try and let me know how it works?

The usage will be something like…

  1. Map the geometry of each of your features to a PreparedGeometry by calling the new makePrepared() method, and store those values somewhere alongside the original features/properties.
  2. At each location update use one of the new contains methods on PreparedGeometry to test for containment.

This is the first time I've tried prepared geometry, and it's a little different than the rest of GEOSwift in that the underly GEOS context escapes the scope of a single method call. I need to do a bit more testing & investigation to make sure that's not going to cause any problems, but in the mean time, please give it a try and let me know if you have any feedback.

macdrevx commented 1 year ago

I also wonder whether the STRtree APIs in GEOS might be an even better fit. I will explore that as well (though I'm out of time for today).

Jessers1 commented 1 year ago

Thanks so much! I've implemented your pull request and at this stage (Fingers crossed) with test locations, it seems to work extremely fast and with accuracy however I am yet to test in a live location update sense (During a journey).

Implementation:

`

import Foundation
import MapKit
import GEOSwift
import CoreLocation
import Combine

class SpeedLimitStore: ObservableObject {
    @Published var geoJSON: GeoJSON? = nil
    var preparedGeometries: [PreparedGeometry] = []
    var featureProperties: [[String: Any]] = []

      init() {
          loadSpeedLimits { features in
              self.geoJSON = GeoJSON.featureCollection(FeatureCollection(features: features))
              self.prepareGeometriesAndStoreProperties(features: features)
          }
      }

  struct SpeedLimit {
      let geometry: Geometry
      let speedLimit: Int
  }

  func loadSpeedLimits(completion: @escaping ([Feature]) -> Void) {
      guard let geoJSONURL = Bundle.main.url(forResource: "speed_limits", withExtension: "geojson") else {
          print("GeoJSON file not found")
          return
      }

      do {
          let data = try Data(contentsOf: geoJSONURL)
          let geoJSON = try JSONDecoder().decode(GeoJSON.self, from: data)
          if case let .featureCollection(featureCollection) = geoJSON {
              print("Loaded \(featureCollection.features.count) speed limit features")
              completion(featureCollection.features)
          } else {
              print("Error: GeoJSON data is not a FeatureCollection")
          }
      } catch {
          print("Error parsing GeoJSON data: \(error.localizedDescription)")
      }
  }

  func prepareGeometriesAndStoreProperties(features: [Feature]) {
          for feature in features {
              guard let geometry = feature.geometry,
                    let properties = feature.properties else {
                  continue
              }

              do {
                  let preparedGeometry = try geometry.makePrepared()
                  preparedGeometries.append(preparedGeometry)
                  featureProperties.append(properties)
              } catch {
                  print("Error preparing geometry: \(error)")
              }
          }
      }

} `

And

`

func findSpeedLimit(location: CLLocationCoordinate2D, preparedGeometries: [PreparedGeometry], featureProperties: [[String: Any]]) -> Int? {

  // Create a Point object from the location coordinates
  let point = Point(x: location.longitude, y: location.latitude)

  // Loop through the prepared geometries
  for (index, preparedGeometry) in preparedGeometries.enumerated() {
      do {
          // Check if the prepared geometry contains the point
          if try preparedGeometry.contains(point) {
              // Check if the properties have a speedLimitZoneValue key
              let properties = featureProperties[index]
              if let speedLimit = properties["speedLimitZoneValue"] {
                  let speedLimitString = String(describing: speedLimit)
                  if let number = Int(speedLimitString.components(separatedBy: CharacterSet.decimalDigits.inverted).joined()) {
                      return number
                  }
              } else {
                  print("Feature \(index) has no 'speedLimitZoneValue' property")
              }
          }
      } catch {
          print("Error checking if prepared geometry contains point: \(error)")
      }
  }

  return nil

}

`

As I said before at this stage it looks miles quicker for simple test locations however I will need to test it in a live updating context.

Jessers1 commented 1 year ago

I will mention though that the bootup time of the program still is incredibly slow (and was even before the changes). Is this usual with the GeoSwift library. See my current loadspeedlimits function. I do call it in the App.swift file could that be the issue?

` @main struct SafeDrivingMapsApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var speedLimitStore = SpeedLimitStore()

      var body: some Scene {
          WindowGroup {
              ContentView()
                  .environmentObject(speedLimitStore)
          }
      }
  }

`

Jessers1 commented 1 year ago

Fixed the bootup time by loading the data asynchronously in the background. At this stage prepared geometry seems to be working perfectly.