3

I'm using the iOS Mapbox SDK to create a MGLShapeCollectionFeature from a goejson FeatureCollection data that comes from a 3rd party API.

guard let feature = try? MGLShape(data: jsonData, encoding: String.Encoding.utf8.rawValue) as? MGLShapeCollectionFeature else {
    print("Could not cast to specified MGLShapeCollectionFeature")
    return
}

The problem is that the API sometimes returns an invalid geojson where a single Feature does not contain valid coordinates (see below) and initialising the MGLShape fails with a 'NSInvalidArgumentException', reason: 'A multipoint must have at least one vertex.' which is correct.

Is there a way to filter out and drop those invalid Features within a FeatureCollection other that parsing and fixing the geojson manually?

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "icaoId": "KBOS",
        "airSigmetType": "AIRMET",
        "hazard": "IFR"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [

          ]
        ]
      }
    },
    {
      "type": "Feature",
      "properties": {
        "icaoId": "KSLC",
        "airSigmetType": "AIRMET",
        "hazard": "IFR"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              -106.63,
              49.06
            ],
            [
              -104.12,
              48.95
            ],
            [
              -104.17,
              44.8
            ],
            [
              -106.91,
              46.38
            ],
            [
              -106.63,
              49.06
            ]
          ]
        ]
      }
    }
  ]
}
Jan
  • 7,444
  • 9
  • 50
  • 74
  • `MGLShape` doesn't seem to do so. You might need to do it yourself. Transforming the JSON Data into Struct/Swift object, do a filter to remove the empty coordinates features, and retransform it into Data. – Larme Feb 17 '19 at 11:43

2 Answers2

4

A possible solution is to decode the JSON with Codable into structs, filter the empty items and encode the object back:

struct FeatureCollection : Codable {
    let type : String
    var features : [Feature]
}

struct Feature : Codable {
    let type : String
    let properties : Properties
    let geometry : Geometry
}

struct Properties : Codable {
    let icaoId, airSigmetType, hazard : String
}

struct Geometry : Codable {
    let type : String
    let coordinates : [[[Double]]]
}

do {
    var result = try JSONDecoder().decode(FeatureCollection.self, from: jsonData)
    let filteredFeatures = result.features.filter{$0.geometry.coordinates != [[]]}
    result.features = filteredFeatures
    let filteredData = try JSONEncoder().encode(result)
    guard let feature = try? MGLShape(data: filteredData, encoding: String.Encoding.utf8.rawValue) as? MGLShapeCollectionFeature else {
        print("Could not cast to specified MGLShapeCollectionFeature")
        return
    }
} catch {
    print(error)
}
vadian
  • 274,689
  • 30
  • 353
  • 361
  • That could work but the problem is that the shapes have many properties that I don't want to decode into a type safe model yet I want them to be accessible via the Mapbox `MGLFeature` `attributes`. I would lost lose them when decoding and recoding with `Codable` – Jan Feb 18 '19 at 09:00
1

As you suggested, I did the filtering myself and wrote this extension on Data

extension Data {

    func removeEmptyCoordinates() throws -> Data {
        guard var geojson = try JSONSerialization.jsonObject(with: self, options: []) as? [String: Any] else {
            return self
        }
        fix(geojson: &geojson,
            processFeatureIf: NSPredicate(format: "geometry.type == 'Polygon'"),
            keepFeatureIf: NSPredicate(format: "%K[0][SIZE] >= 2", "geometry.coordinates"))
        return try JSONSerialization.data(withJSONObject: geojson, options: [])
    }

    private func fix(geojson: inout [String: Any], processFeatureIf: NSPredicate, keepFeatureIf: NSPredicate) {
        guard let type = geojson["type"] as? String, type == "FeatureCollection" else {
            // "Not a FeatureCollection"
            return
        }
        // "No features to fix"
        guard let features = geojson["features"] as? [[String: Any]] else { return }

        let filtered = features.filter { feature in
            if !processFeatureIf.evaluate(with: feature) {
                // not processing
                return true
            }
            return keepFeatureIf.evaluate(with: feature)
        }
        geojson["features"] = filtered
    }
}
Jan
  • 7,444
  • 9
  • 50
  • 74