[Swift] MapViewで山手線の各駅にピンを立てて線で結んでみた

# MapViewでピンを立てて線で結ぶにはどうするの？

散歩した軌跡をマップに表示したいと思い、MapViewでどう実現するのかを調べた。
そこで得た成果として『山手線の各駅にピンを立てて線で結ぶ』コードを書いたてみた。

XcodeのPlaygroundで動かしました。

<iframe width="400" height="400" src="https://www.youtube.com/embed/BMv7lYXc7Qw?rel=0" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>


# ピンの立て方

アノテーションが正しい呼び方。（`PinAnnotation`はDeprecated）

```swift
let annotation = MKPointAnnotation()
let (latitude, longitude) = ピンを立てる座標（経度、緯度）
annotation.title = title
annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
mapView.addAnnotation(annotation)
```

座標データ`MKPointAnnotation`を用意して、`mapView.addAnnotation()`呼ぶだけ。
今回は使っていないが、複数の座標にまとめてピンを立てるなら、座標データを配列にして`mapView.addAnnotations()`を呼ぶ。

```swift
let annotations: [MKPointAnnotation] = [
　座標データを配列にする
]
mapView.addAnnotations(annotations)
```

ピンの色などは後ほど説明するデリゲートメソッドで指定する。




# 座標間の結び方

線を引いていく順に座標を配列にして`polyline`を作成し、`mapView.addOverlay()`を呼ぶ。

```swift
let coordinates: [CLLocationCoordinate2D] = [
    線で結びたい座標データをCLLocationCoordinate2Dの配列にする
]
let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
mapView.addOverlay(polyline, level: .aboveRoads)
```

線の色や太さなどは、次に説明するデリゲートメソッドで指定する。


# MapViewデリゲートメソッド

```swift
class MapDelegate: NSObject, MKMapViewDelegate {
    // ピンの色やイメージを指定する
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        let identifier = "annotation"
        if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) {
            annotationView.annotation = annotation
            return annotationView
        } else {
            let annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
            annotationView.markerTintColor = .blue
         // annotationView.glyphImage = 独自バルーンのイメージ
            return annotationView
        }
    }

    //線の色や太さを指定する
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        let renderer = MKPolylineRenderer(overlay: overlay)
        renderer.strokeColor = .orange
        renderer.lineWidth = 3.0
        return renderer
    }
}
```


# 縮尺の指定

これを指定しないと、冒頭動画の初めのように日本列島全体が見渡せるかなり高所から見たマップが表示されます。
方法は、見せたい複数の座標点をカバーする範囲を`MKCoordinateRegion`で指定して、`mapView.setRegion()`を呼びます。
線で結ぶときに作った`Polyline`から`boundingMapRect`プロパティで得られる領域を引数とします。

```diff_swift
  let coordinates: [CLLocationCoordinate2D] = [
      線で結びたい座標データをCLLocationCoordinate2Dの配列にする
  ]
  let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
  mapView.addOverlay(polyline, level: .aboveRoads)

+ var region = MKCoordinateRegion(polyline.boundingMapRect)
+ region.span.latitudeDelta *= 1.2
+ region.span.longitudeDelta *= 1.2
+ mapView.setRegion(region, animated: true)
```

マップの上下左右に余裕ができるように`1.2`倍しています。


# 『山手線の各駅にピンを立てて線で結ぶ』コード

最後に、冒頭の動画で使ったコード全体を載せておきます。
タイマーを使って、3秒ごとに駅にピンを立て線で結びます。
縮尺(MKCoordinateRegion)は、駅にピンを立てるたびに領域にunionで付け加えています。
各駅のデータは、コードの末尾に配列で定義しています。


```swift
import PlaygroundSupport
import MapKit

let mapView = MKMapView()
mapView.frame = CGRect(x: 0, y: 0, width: 600, height: 600)
PlaygroundPage.current.liveView = mapView
PlaygroundPage.current.needsIndefiniteExecution = true

let mapDelegate = MapDelegate()
mapView.delegate = mapDelegate

var stations = 0
var boundingMapRect = MKMapRect()
let timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { timer in
    if stations > YamanoteLineCoordinates.count {
        timer.invalidate()
    } else {
        let annotation = MKPointAnnotation()
        let (title, latitude, longitude) = YamanoteLineCoordinates[stations % YamanoteLineCoordinates.count]
        annotation.title = title
        annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
        if stations < YamanoteLineCoordinates.count {
            mapView.addAnnotation(annotation)
        }
        
        if stations > 0 {
            let (_, latitude, longitude) = YamanoteLineCoordinates[stations - 1]
            let coordinate2 = [
                CLLocationCoordinate2D(latitude: latitude, longitude: longitude),
                annotation.coordinate
            ]
            let polyline = MKPolyline(coordinates: coordinate2, count: coordinate2.count)
            mapView.addOverlay(polyline, level: .aboveRoads)
            boundingMapRect.isEmpty
            boundingMapRect = boundingMapRect.isEmpty ? polyline.boundingMapRect : boundingMapRect.union(polyline.boundingMapRect)

            var region = MKCoordinateRegion(boundingMapRect)
            region.span.latitudeDelta = region.span.latitudeDelta * 1.2
            region.span.longitudeDelta = region.span.longitudeDelta * 1.2
            mapView.setRegion(region, animated: true)
        }
    }
    stations += 1
}

class MapDelegate: NSObject, MKMapViewDelegate {
    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        let renderer = MKPolylineRenderer(overlay: overlay)
        renderer.strokeColor = .orange
        renderer.lineWidth = 3.0
        return renderer
    }
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        let identifier = "annotation"
        if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) {
            annotationView.annotation = annotation
            return annotationView
        } else {
            let annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
            annotationView.markerTintColor = .blue
            return annotationView
        }
    }
}

var YamanoteLineCoordinates: [(String, Double, Double)] {
[ //(駅名, 緯度, 経度),
    ("東京", 35.681382, 139.766084),
    ("有楽町", 35.675069, 139.763328),
    ("新橋", 35.665498, 139.75964),
    ("浜松町", 35.655646, 139.756749),
    ("田町", 35.645736, 139.747575),
    ("高輪ゲートウェイ", 35.6355, 139.7407),
    ("品川", 35.630152, 139.74044),
    ("大崎", 35.6197, 139.728553),
    ("五反田", 35.626446, 139.723444),
    ("目黒", 35.633998, 139.715828),
    ("恵比寿", 35.64669, 139.710106),
    ("渋谷", 35.658517, 139.701334),
    ("原宿", 35.670168, 139.702687),
    ("代々木", 35.683061, 139.702042),
    ("新宿", 35.690921, 139.700258),
    ("新大久保", 35.701306, 139.700044),
    ("高田馬場", 35.712285, 139.703782),
    ("目白", 35.721204, 139.706587),
    ("池袋", 35.728926, 139.71038),
    ("大塚", 35.731401, 139.728662),
    ("巣鴨", 35.733492, 139.739345),
    ("駒込", 35.736489, 139.746875),
    ("田端", 35.738062, 139.76086),
    ("西日暮里", 35.732135, 139.766787),
    ("日暮里", 35.727772, 139.770987),
    ("鶯谷", 35.720495, 139.778837),
    ("上野", 35.713768, 139.777254),
    ("御徒町", 35.707438, 139.774632),
    ("秋葉原", 35.698683, 139.774219),
    ("神田", 35.69169, 139.770883),
] }
```


- Xcode 14.3
- Swift  5.8
- macOS 13.3.1(a) 


# おしまい

点の集合でも軌跡は表現できるが、やはり線で結びたいと思い調べて実装しました。（ピンを立てずに線を引くだけで軌跡を表現）



以上です