Rajesh
Rajesh

Reputation: 937

how to highlight countries in ios maps

I am building an app in which I have to highlight some countries dynamically in the world map. like this

enter image description here

In short I want to customize the whole view of ios maps as shown in the images. can this be done using MapKit or is there any other method. Thanks in advance

Upvotes: 22

Views: 8517

Answers (4)

Obrienser
Obrienser

Reputation: 29

Highlight countries, SwiftUI version

Here is a geojson: https://github.com/datasets/geo-boundaries-world-110m/blob/main/countries.geojson

import SwiftUI
import CoreLocation

struct GeoJson: Codable {
    let type: String
    let features: [GeoJsonFeature]
}

struct GeoJsonFeature: Codable {
    let type: String
    let geometry: GeoJsonGeometry
}

struct GeoJsonGeometry: Codable {
    let type: String
    let coordinates: GeoJsonCoordinates
}

struct GeoJsonCoordinates: Codable {
    var point: [Double]?
    var line: [[Double]]?
    var polygon: [[[Double]]]?
    var multiPolygon: [[[[Double]]]]?

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let point = try? container.decode([Double].self) {
            self.point = point
            return
        }
        if let line = try? container.decode([[Double]].self) {
            self.line = line
            return
        }
        if let polygon = try? container.decode([[[Double]]].self) {
            self.polygon = polygon
            return
        }
        if let multiPolygon = try? container.decode([[[[Double]]]].self) {
            self.multiPolygon = multiPolygon
            return
        }
        throw DecodingError.valueNotFound(Self.self, .init(codingPath: [], debugDescription: ""))
    }
}

struct ContentView: View {
    @State private var shapes: [PolygonShape] = []
    
    var body: some View {
        ZStack {
            ForEach(shapes) { shape in
                Path { path in
                    guard let firstPoint = shape.points.first else { return }
                    path.move(to: firstPoint)
                    for point in shape.points {
                        path.addLine(to: point)
                    }
                    path.closeSubpath()
                }
                .fill(shape.color)
            }
        }
        .onAppear {
            loadGeoJson()
        }
        .frame(width: 300, height: 200)
        .border(Color.black, width: 1)
    }
    
    func loadGeoJson() {
        guard let url = Bundle.main.url(forResource: "countries", withExtension: "geojson"),
              let data = try? Data(contentsOf: url),
              let geoJson = try? JSONDecoder().decode(GeoJson.self, from: data)
        else {
            return
        }
        
        for feature in geoJson.features {
            let geometry = feature.geometry
            let randomColor = Color(hue: Double.random(in: 0...1), saturation: 1, brightness: 1)

            if geometry.type == "Polygon", let coordinates = feature.geometry.coordinates.polygon {
                for polygon in coordinates {
                    addShape(polygon: polygon, color: randomColor)
                }
            }
            if geometry.type == "MultiPolygon", let coordinates = feature.geometry.coordinates.multiPolygon {
                for multiPolygon in coordinates {
                    for polygon in multiPolygon {
                        addShape(polygon: polygon, color: randomColor)
                    }
                }
            }
        }
    }

    func addShape(polygon: [[Double]], color: Color) {
        let polygonCoordinates: [CLLocationCoordinate2D] = polygon.map { coordinate in
            CLLocationCoordinate2D(latitude: coordinate[1], longitude: coordinate[0])
        }
        let points: [CGPoint] = polygonCoordinates.map { coordinate in
            coordinateToPoint(coordinate)
        }
        let shape = PolygonShape(points: points, color: color)
        shapes.append(shape)
    }

    func coordinateToPoint(_ coordinate: CLLocationCoordinate2D) -> CGPoint {
        let width = 300.0
        let height = 200.0
        let x = (coordinate.longitude + 180.0) * (width / 360.0)
        let latitudeRadians = coordinate.latitude * .pi / 180.0
        let n = log(tan((.pi / 4.0) + (latitudeRadians / 2.0)))
        let y = (height / 2.0) - (width * n / (2.0 * .pi))
        return CGPoint(x: x, y: y)
    }
}

struct PolygonShape: Identifiable {
    let id = UUID()
    let points: [CGPoint]
    let color: Color
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

You can also add: let properties: [String: AnyCodableGeoJson] to get more data from .geojson

struct GeoJsonFeature: Codable {
    let type: String
    let geometry: GeoJsonGeometry
    let properties: [String: AnyCodableGeoJson]
}

Upvotes: 0

Obrienser
Obrienser

Reputation: 29

According to this data, Crimea belongs to Russia, which is not true.

Here is a geojson with countries coordinates: https://github.com/nvkelso/natural-earth-vector/blob/master/geojson/ne_110m_admin_0_countries.geojson that should fit the needs for the task in this question.

Correct .geojson: https://github.com/datasets/geo-boundaries-world-110m/blob/main/countries.geojson

Upvotes: 0

Leszek Szary
Leszek Szary

Reputation: 10308

I recently looked on some possibilities for implementing something similar and you could now actually implement something like this quite easily without additional libraries. To do that you need countries borders coordinates data and you can get that from Natural Earth (https://www.naturalearthdata.com/). This data is unfortunately in format that cannot be easily read on iOS but you can convert it to a json format or to be precise geojson format with QGIS (https://www.qgis.org/en/site/forusers/download.html) or you can just use geojson that someone else converted. Here is a geojson with countries coordinates: https://github.com/nvkelso/natural-earth-vector/blob/master/geojson/ne_110m_admin_0_countries.geojson that should fit the needs for the task in this question.

We can parse geojson file and draw a map with CAShapeLayers. Below is an example code that draws a map with random colors for shapes from geojson which gave me the result as on the screenshot below which I think is quite close to expected result so modifying it should be quite easy.

map_from_geojson

Geojson parsing and map drawing code example:

import UIKit
import CoreLocation

struct GeoJson: Codable {
    let type: String
    let features: [GeoJsonFeature]
}

struct GeoJsonFeature: Codable {
    let type: String
    let geometry: GeoJsonGeometry
}

struct GeoJsonGeometry: Codable {
    let type: String
    let coordinates: GeoJsonCoordinates
}

struct GeoJsonCoordinates: Codable {
    var point: [Double]?
    var line: [[Double]]?
    var polygon: [[[Double]]]?
    var multiPolygon: [[[[Double]]]]?

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let point = try? container.decode([Double].self) {
            self.point = point
            return
        }
        if let line = try? container.decode([[Double]].self) {
            self.line = line
            return
        }
        if let polygon = try? container.decode([[[Double]]].self) {
            self.polygon = polygon
            return
        }
        if let multiPolygon = try? container.decode([[[[Double]]]].self) {
            self.multiPolygon = multiPolygon
            return
        }
        throw DecodingError.valueNotFound(Self.self, .init(codingPath: [], debugDescription: ""))
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        loadGeoJson()
    }

    func loadGeoJson() {
        guard let url = Bundle.main.url(forResource: "ne_110m_admin_0_countries", withExtension: "geojson"),
              let data = try? Data(contentsOf: url),
              let geoJson = try? JSONDecoder().decode(GeoJson.self, from: data)
        else {
            return
        }
    
        for feature in geoJson.features {
            let geometry = feature.geometry
        
            let randomColor = UIColor(hue: Double.random(in: 0...1), saturation: 1, brightness: 1, alpha: 1)
        
            // check https://macwright.com/2015/03/23/geojson-second-bite.html for other types info if needed
            // note that below we do not support it exactly as it should (internal cutouts in polygons are ignored)
            // but for needed purpose it should not make a big difference
        
            if geometry.type == "Polygon", let coordinates = feature.geometry.coordinates.polygon {
                for polygon in coordinates {
                    addShape(polygon: polygon, color: randomColor)
                }
            }
            if geometry.type == "MultiPolygon", let coordinates = feature.geometry.coordinates.multiPolygon {
                for multiPolygon in coordinates {
                    for polygon in multiPolygon {
                        addShape(polygon: polygon, color: randomColor)
                    }
                }
            }
        }
    }

    func addShape(polygon: [[Double]], color: UIColor) {
        let polygonCoordinates: [CLLocationCoordinate2D] = polygon.map { coordinate in
            CLLocationCoordinate2D(latitude: coordinate[1], longitude: coordinate[0])
        }
        let points: [CGPoint] = polygonCoordinates.map { coordinate in
            coordinateToPoint(coordinate)
        }
        let path = UIBezierPath()
        path.move(to: points[0])
        for point in points {
            path.addLine(to: point)
        }
        path.close()
    
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = path.cgPath
        shapeLayer.fillColor = color.cgColor
        shapeLayer.position = CGPoint(x: 50, y: 200)
    
        view.layer.addSublayer(shapeLayer)
    }

    func coordinateToPoint(_ coordinate: CLLocationCoordinate2D) -> CGPoint {
        let width = 300.0
        let height = 200.0
        let x = (coordinate.longitude + 180.0) * (width / 360.0)
        let latitudeRadians = coordinate.latitude * .pi / 180.0
        let n = log(tan((.pi / 4.0) + (latitudeRadians / 2.0)))
        let y = (height / 2.0) - (width * n / (2.0 * .pi))
        return CGPoint(x: x, y: y)
    }
}

Upvotes: 1

incanus
incanus

Reputation: 5128

You want to look into the Mapbox iOS SDK which will allow you to do this and more with a MapKit-like API. In particular, you will want to quickly make a custom map with TileMill using the provided Natural Earth data set for world country borders, enable UTFGrid interactivity so that the tapped regions can be identified, and use the RMShape class on an RMAnnotation onto the map view to add/color country polygons as needed. This sounds a little complex but the tools exist, are entirely free and open source, and I can help you with this process.

Upvotes: 9

Related Questions