VKDefault -Triangulator failed to fully triangulate polygon MapKit
Asked Answered
E

2

7

I'm having an issue where I get the warning [VKDefault] Triangulator failed to fully triangulate polygon MapKit when zooming in and out of specific regions on my map.

I'm creating the polygons from json data that can either contain a MKPolygon or a MKMultiPolygon. Only several of the 200 + polygons have the issue.

For me the data looks fine. Ive tried to remove duplicates in the coordinates arrays if they contain any but had no success. Maybe I wasn't doing it right because a MKMultiPolygon is created from a [[[[Double]]]]

Here is a link link to the project - https://www.dropbox.com/s/zwy08upcne11dyy/VectorMaps%203.zip?dl=0

This is just a prototype so please don't hammer me on force unwrapping.

This is what a region with the warning looks like.

enter image description here

I added a list of regions I'm having issues with in the ViewController.

Extra credit for solving the warning VectorMaps[1611:116104] [VKDefault] Style Z is requested for an invisible rect

struct SubRegion: Decodable {
    var id: Int
    var name: String
    var geometry: Geometry

    enum CodingKeys: String, CodingKey {
        case id, name, geometry = "geojson_geometry"
    }
}

enum PolygonType: String {
    case multiPolygon = "MultiPolygon"
    case polygon = "Polygon"
}

struct Geometry: Decodable {
    var type: String
    var coordinates: Array<Any>
    var convexHullGeometry: ConvexGeometry?

    enum CodingKeys: String, CodingKey {
        case type, coordinates, convexHullGeometry = "convex_hull_geojson_geometry"
    }

    enum ParseError: Error {
        case notRecognizedType(Any)
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        type = try container.decode(String.self, forKey: .type)

        switch type {
        case "Polygon":
            coordinates = try container.decode([[[Double]]].self, forKey: .coordinates)
        case "MultiPolygon":
            coordinates = try container.decode([[[[Double]]]].self, forKey: .coordinates)
        default:
            throw ParseError.notRecognizedType(type)
        }
    }
}

struct ConvexGeometry: Codable {
    var coordinates: [[[Double]]]
}

class ViewController: UIViewController {
    var activeMultiRenders = [MKMultiPolygonRenderer]()
    var activeRenders = [MKPolygonRenderer]()
    var mapView: MKMapView!

    let pinView = LocationPinView()
    var showMarker = false
    var showAnnotation = false

    var this = MKPointAnnotation()

    typealias Polygon = [[[Double]]]
    typealias MultiPolygon = [[[[Double]]]]

    let staticMarkerButton: UIButton = {
        let button = UIButton()
        button.setTitle("Show static marker", for: .normal)
        button.addTarget(self, action: #selector(addMarker), for: .touchUpInside)
        button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15)
        button.setTitleColor(.blue, for: .normal)
        button.backgroundColor = .white
        button.layer.cornerRadius = 5
        return button
    }()

    let mapButton: UIButton = {
        let button = UIButton()
        button.setTitle("Radius Map", for: .normal)
        button.addTarget(self, action: #selector(visitRadius), for: .touchUpInside)
        button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15)
        button.setTitleColor(.blue, for: .normal)
        button.backgroundColor = .white
        button.layer.cornerRadius = 5
        return button
    }()

    let showLabelButton: UIButton = {
        let button = UIButton()
        button.setTitle("Show labels", for: .normal)
        button.addTarget(self, action: #selector(addAnnotations), for: .touchUpInside)
        button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 15, bottom: 10, right: 15)
        button.setTitleColor(.blue, for: .normal)
        button.backgroundColor = .white
        button.layer.cornerRadius = 5
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        mapView = MKMapView(frame: self.view.frame)
        mapView.isRotateEnabled = false
        mapView.mapType = .standard

        addPolygonsToMap()

        mapView.delegate = self
        self.view.addSubview(mapView)
        
        let tap = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
        self.mapView.addGestureRecognizer(tap)

        self.view.addSubview(pinView)
        pinView.frame = CGRect(x: mapView.center.x, y: mapView.center.y - pinView.frame.height,
                               width: pinView.frame.width, height: pinView.frame.height)
        pinView.isHidden = true

        view.addSubview(staticMarkerButton)
        staticMarkerButton.translatesAutoresizingMaskIntoConstraints = false
        staticMarkerButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 100).isActive = true
        staticMarkerButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true

        view.addSubview(mapButton)
        mapButton.translatesAutoresizingMaskIntoConstraints = false
        mapButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -100).isActive = true
        mapButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true

        view.addSubview(showLabelButton)
        showLabelButton.translatesAutoresizingMaskIntoConstraints = false
        showLabelButton.leadingAnchor.constraint(equalTo: mapButton.trailingAnchor, constant: 10).isActive = true
        showLabelButton.topAnchor.constraint(equalTo: mapButton.topAnchor).isActive = true
    }

    func addPolygonsToMap() {
        let fileURL = Bundle.main.url(forResource: "subregion", withExtension: "json")!
        let decoder = JSONDecoder()

        do {
            let data = try Data(contentsOf: fileURL)
            let subregions = try decoder.decode([SubRegion].self, from: data)

            for each in subregions {
                let geometry = each.geometry

//                if !badData.contains(each.name) {
//                    continue
//                }

                // Type is either a Polygon or MultiPolygon
                let geometryType = each.geometry.type

                if geometryType == PolygonType.polygon.rawValue {
                    // Cast the coordinates to a Polygon type-alias.
                    let coords = geometry.coordinates as! Polygon
                    let lCoords = getCoordinatesFromPolygonArray(coords)
                                        
                    // Create a MKPolygon with locationCoordinates
                    let polygon = MKPolygon(coordinates: lCoords, count: lCoords.count)

                    // Add the MKPolygon to the map
                    mapView.addOverlay(polygon)
                } else if geometryType == PolygonType.multiPolygon.rawValue {
                    // Cast the coordinates to a MultiPolygon type-alias.
                    let coords = geometry.coordinates as! MultiPolygon
                    
                    var polygons:[MKPolygon] = []
                    coords.forEach { polygon in

                        let lCoords = getCoordinatesFromPolygonArray(polygon)

                        // Create a MKPolygon with locationCoordinates
                        let polygon = MKPolygon(coordinates: lCoords, count: lCoords.count)

                        polygons.append(polygon)
                    }
                    
                    let multiPolygon = MKMultiPolygon(polygons)
                    mapView.addOverlay(multiPolygon)
                }
            }
        } catch let error {
            print("Error", error)
        }
    }

    let badData = ["Central Coast", "Dapto and Port Kembla", "Wellington", "South West Perth", "East Arnhem", "Charters Towers", "Ormeau", "Murray River", "Wodonga", "Bruny Island", "Katherine", "Far North", "Tweed Valley", "Richmond Valley", "South East Coast", "Gympie", "Meander Valley"]
    
    private func getCoordinatesFromPolygonArray(_ polygon: Polygon) -> [CLLocationCoordinate2D] {
        var locationCoordinates = [CLLocationCoordinate2D]()

        if let coordinates = polygon.first?.map({ CLLocationCoordinate2D(latitude: $0[1] , longitude: $0[0]) }) {
            locationCoordinates.append(contentsOf: coordinates)
        }

        return locationCoordinates
    }

    
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        if let polygon = overlay as? MKPolygon {
            let renderer = MKPolygonRenderer(polygon: polygon)
            renderer.strokeColor = UIColor.blue.withAlphaComponent(0.4)
            return renderer
        } else if let multiPolygon = overlay as? MKMultiPolygon {
            let renderer = MKMultiPolygonRenderer(multiPolygon: multiPolygon)
            renderer.strokeColor = UIColor.blue.withAlphaComponent(0.4)
            return renderer
        }
        
        return MKOverlayRenderer(overlay: overlay)
    }

    @objc func mapTapped(_ gesture: UITapGestureRecognizer) {
        let point = gesture.location(in: self.mapView)
        let coordinate = self.mapView.convert(point, toCoordinateFrom: nil)
        let mappoint = MKMapPoint(coordinate)

        for renderer in self.mapView.polygonRenderers {
            let tapPoint = renderer.point(for: mappoint)
            if renderer.path.contains(tapPoint) && activeRendersContains(renderer) {
                renderer.strokeColor = UIColor.blue.withAlphaComponent(0.4)
                renderer.fillColor = UIColor.clear
                removeActive(renderer)

            } else if renderer.path.contains(tapPoint) {
                renderer.fillColor = UIColor.blue.withAlphaComponent(0.4)
                renderer.strokeColor = UIColor.blue
                appendActive(renderer)
            }
        }
    }

    func activeRendersContains(_ renderer: MKOverlayPathRenderer) -> Bool {
        if let multiRenderer = renderer as? MKMultiPolygonRenderer {
            return activeMultiRenders.contains(multiRenderer)
        } else if let polyRenderer = renderer as? MKPolygonRenderer {
            return activeRenders.contains(polyRenderer)
        } else {
            return false
        }
    }

    func appendActive(_ renderer: MKOverlayPathRenderer) {
        if let multiRenderer = renderer as? MKMultiPolygonRenderer {
            activeMultiRenders.append(multiRenderer)
        } else if let polyRenderer = renderer as? MKPolygonRenderer {
            activeRenders.append(polyRenderer)
        }
    }

    func removeActive(_ renderer: MKOverlayPathRenderer) {
        if let multiRenderer = renderer as? MKMultiPolygonRenderer {
            activeMultiRenders.removeAll(where: {$0 == multiRenderer})
        } else if let polyRenderer = renderer as? MKPolygonRenderer {
            activeRenders.removeAll(where: {$0 == polyRenderer})
        }
    }

    @objc func addMarker() {
        showMarker.toggle()

        if showMarker {
            staticMarkerButton.setTitle("Hide marker", for: .normal)
            pinView.isHidden = false
        } else {
            staticMarkerButton.setTitle("Show marker", for: .normal)
            pinView.isHidden = true
        }
    }

    @objc func visitRadius() {
        let radiusVC = RadiusViewController()
        radiusVC.modalPresentationStyle = .fullScreen
        present(radiusVC, animated: false, completion: nil)
    }

    @objc func addAnnotations() {
        showAnnotation.toggle()

        if showAnnotation {
            // Gets the coordinates and titles of each polygon renderer.
            let details = mapView.activePointDetailsFor(activeRenders)

            // Creates and adds annotations to the map with given details.
            mapView.addAnnotationsWith(details)
        } else {
            mapView.removeAllAnnotations()
        }
    }
}

extension ViewController: MKMapViewDelegate {

    func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
        pinView.pinPosition = .up
    }

    func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
        if showMarker {
            pinView.pinPosition = .down
        }
    }

    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        let annotationView = LabelAnnotationView(annotation: annotation, reuseIdentifier: annotation.title!)
        return annotationView
    }
}

extension MKMapView {
    var polygonRenderers: [MKOverlayPathRenderer] {
        var renders = [MKOverlayPathRenderer]()
        for overlay in self.overlays {
            if let polygon = overlay as? MKPolygon {
                guard let renderer = self.renderer(for: polygon) as? MKPolygonRenderer else { continue }
                renders.append(renderer)
            } else if let multiPolygon = overlay as? MKMultiPolygon {
                guard let renderer = self.renderer(for: multiPolygon) as? MKMultiPolygonRenderer else { continue }
                renders.append(renderer)
            }
        }
        return renders
    }

    typealias PointDetails = (coordinate: CLLocationCoordinate2D ,title: String)

    func activePointDetailsFor(_ renders: [MKPolygonRenderer]) -> [PointDetails] {
        var details = [PointDetails]()

        for each in renders {
            let title: String = each.polygon.title ?? ""
            let coordinate = each.polygon.coordinate

            let detail = (coordinate: coordinate ,title: title)
            details.append(detail)
        }

        return details
    }

    func addAnnotationsWith(_ details: [PointDetails]) {
        for each in details {
            let annotation = MKPointAnnotation()
            annotation.coordinate = each.coordinate
            annotation.title = each.title
            self.addAnnotation(annotation)
        }
    }

    func removeAllAnnotations() {
        let annotations = self.annotations
        self.removeAnnotations(annotations)
    }
}
 
Easterner answered 20/9, 2021 at 2:28 Comment(3)
Regarding the Style Z is requested for an invisible rect noise in the logs, this question/answer (and particularly the answer for adding a setting specific to the Simulator, by @cdhun) might be helpful.Heathenize
A GeoJSON Polygon consists of an outer polygon and optional interior polygons describing holes. As long as you are not using this initialiser developer.apple.com/documentation/mapkit/mkpolygon/1451945-init you're missing the holes. Be aware that holes in GeoJSON have clockwise coordinates and outer polygons counter clockwise coordinates, which might or might not be relevant.Paramo
You have an invalid polygon. Probably concavity/convexity is affected by the compression algorithm (while zooming). Can you provide a complete dataset of one of the broken polygons (the one with fewer points would be better)?Detinue
M
3

I'm still figuring it out but in iOS 16.1 I'm getting a similar issue. However, I have removed the drawing of polygons and annotations from my map.

List has 13 nodes:
    33 34 35 37 30 32 21 23 24 26 38 4 0 
[VKDefault] Building failed to triangulate!
Wrapped around the polygon without finishing... :-(
List has 15 nodes:
    10 17 23 32 7 8 1 30 27 28 32 26 21 13 9 
[VKDefault] Building failed to triangulate!
Wrapped around the polygon without finishing... :-(
List has 14 nodes:
    55 50 43 44 38 39 41 50 52 53 48 35 37 54 
 [VKDefault] Building failed to triangulate!
 Missing MeshRenderables for ground mesh layer for (4/4) of ground tiles. Tile debug info: (Key: 7.4.4.255 t:34 kt:0, 
Has mesh errors: 0, MeshInstance count: 2, PendingMaterial count: 2, Invisible MeshInstances count: 0 | Key: 7.5.4.255 t:34 kt:0, 
Has mesh errors: 0, MeshInstance count: 2, PendingMaterial count: 2, Invisible MeshInstances count: 0 | Key: 8.4.4.255 t:34 kt:0, 
Has mesh errors: 0, MeshInstance count: 2, PendingMaterial count: 2, Invisible MeshInstances count: 0 | Key: 8.5.4.255 t:34 kt:0,
Has mesh errors: 0, MeshInstance count: 2, PendingMaterial count: 2, Invisible MeshInstances count: 0)

When I have annotations and polygons, the app freezes but does not crash.

I also get

[VKDefault] Exceeded Metal Buffer threshold of 50000 with a count of 50892 resources,  
  pruning resources now (Time since last prune:6.497636): Assertion with expression - false : 
  Failed in file - /Library/Caches/com.apple.xbs/Sources/VectorKit/src/MDMapEngine.mm line - 1363
Exceeded Metal Buffer threshold of 50000 with a count of 51462 resources, 
  pruning resources now (Time since last prune:6.809683): Assertion with expression - false :
  Failed in file - /Library/Caches/com.apple.xbs/Sources/VectorKit/src/MDMapEngine.mm line - 1363

The only solution so far has been

mapView.mapType = MKMapType.hybrid
mapView.mapType = MKMapType.standard

to clear the cache of the app.

Mover answered 4/11, 2022 at 12:3 Comment(4)
Since iOS 16.1 my app which uses MapKit (but does not draw any polygons) has started hanging intermittently on the map. In the log I see "Exceeded Metal Buffer threshold". I will try your cache clearing suggestion - thanks!Atalee
Thanks for sharing that, I've tested polygon MapKit projects I have that I know are stable and the same issue arises. Will check what happens without polygons.Mover
@Atalee Did you have success with this? I have tried it, but aside from a flickering map, I see no difference in terms of the app freezing up on me.Categorize
@Categorize no, I have this workaround in my app and I still see occasional hangs. It might have improved slightly but the bug is still there. Note that I have no polygons just MKPointAnnotation and MKGeodesicPolyline.Atalee
P
1

I think MapKit have problems with polygons that have holes, I have tried the same area in geojson.io and I verify the your geodata is not corrupted.

map

Pablo answered 30/9, 2021 at 3:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.