はじめに
前々から日本地図描きたいな〜と思ってたので今回はSwiftUIで綺麗な日本地図を描きたいと思います。
最終的にはこんな感じの地図が描けます![]()
データの取得・加工については下記記事を参考にさせていただきました![]()
手順
- 地図データをダウンロード
- 地図データを加工
- 地図データを軽量化
- SwiftUIで描く
地図データをダウンロード
下記のNatural Earthから地図データをダウンロードします。
ダウンロード手順。
- Downloads
- Large scale data, 1:10m
- Cultural
- Admin 1 – States, Provinces
- Download states and provinces
地図データを加工
下記のQGIS(3.44.5)を使って地図データを加工します。
SHPファイルを開く。
- レイヤ
- レイヤの追加
- ベクタレイヤの追加
- ダウンロードしたshpファイルを選択
- 追加
日本以外を削除。
- 編集モードに切り替え
- 地物の選択に切り替え
- 不要な地形を削除
沖縄を任意の位置に調整。
- 編集
- ジオメトリを編集
- 地物を移動
GeoJSONでエクスポート。
- レイヤ選択
- 右クリック
- エクスポート
- 新規ファイルに地物を保存
- 形式でGeoJson選択
- 保存
地図データを軽量化
このままだと描画にめちゃくちゃ時間がかかるので下記のmapshaperを使って地図データを軽量化します。
軽量化してエクスポート。
- Simplify
- スライダーで任意の値に軽量化
- Export
- GeoJson選択
- Export
SwiftUIで描く
エクスポートしたjsonファイルはこんな感じです。
{
"type": "Feature",
"geometry": { ... }, // 形(ポリゴン、線、点など)
"properties": { ... } // 属性情報(名前、コードなど)
}
"geometry": {
"type": "Polygon",
"coordinates": [
[ [lon, lat], [lon, lat], ... ], // 外周
]
}
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[ [lon, lat], [lon, lat], ... ] // 外周
],
[
[ [lon, lat], ... ] // 別の島など
]
]
}
あまり詳しく知らないですがおそらく内陸や飛地のない県はPolygonで島とかある県はMultiPolygonになるんだと思います。これを考慮して県ごとの緯度経度を取得します。
こんな感じです。
import Foundation
@Observable final class JapanMapStore {
var polygons: [[[CGPoint]]] = []
init() {
loadData()
}
private func loadData() {
guard let geoJSONURL = Bundle.main.url(forResource: "japan2", withExtension: "json"),
let data = try? Data(contentsOf: geoJSONURL),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let features = json["features"] as? [[String: Any]] else {
return
}
let polygonPrefectures: [[[CGPoint]]] = features.compactMap { feature in
guard let geometry = feature["geometry"] as? [String: Any],
let type = geometry["type"] as? String,
type == "Polygon",
let coordinates = geometry["coordinates"] as? [[[Double]]] else {
return nil
}
let points: [CGPoint] = coordinates.flatMap { ring in
ring.map { coord in
let lat = coord[1]
let lon = coord[0]
return CGPoint(x: lat, y: lon)
}
}
return [points]
}
let multiPolygonPrefectures: [[[CGPoint]]] = features.compactMap { feature in
guard let geometry = feature["geometry"] as? [String: Any],
let type = geometry["type"] as? String,
type == "MultiPolygon",
let coordinates = geometry["coordinates"] as? [[[[Double]]]] else {
return nil
}
let polygons: [[CGPoint]] = coordinates.map { rings in
rings.map { ring in
ring.map { coord in
let lat = coord[1]
let lon = coord[0]
return CGPoint(x: lat, y: lon)
}
}
}.flatMap { $0 }
return polygons
}
polygons = polygonPrefectures + multiPolygonPrefectures
}
}
次に描画範囲を指定して地図を描きます。何かと使いやすいので正方形の枠を作って描いていきます。
import SwiftUI
struct JpnMapView: View {
@State var store = JapanMapStore()
var body: some View {
Canvas { context, size in
let length = min(size.width, size.height)
let drawRect = CGRect(
x: (size.width - length) / 2,
y: (size.height - length) / 2,
width: length,
height: length
)
store.polygons.forEach {
var path = Path()
$0.forEach { polygon in
path.addLines(polygon.map { point in
transformCoordinates(lat: point.x, lon: point.y, bounds: drawRect)
})
path.closeSubpath()
}
context.fill(path, with: .color(.green))
context.stroke(path, with: .color(.black), lineWidth: 0.3)
}
}
}
private func transformCoordinates(lat: Double, lon: Double, bounds: CGRect) -> CGPoint {
let geoBounds = (minLat: 30.0, maxLat: 46.0, minLon: 124.0, maxLon: 150.0)
let x = (lon - geoBounds.minLon) / (geoBounds.maxLon - geoBounds.minLon) * bounds.width + bounds.origin.x
let y = (1 - (lat - geoBounds.minLat) / (geoBounds.maxLat - geoBounds.minLat)) * bounds.height + bounds.origin.y
return CGPoint(x: x, y: y)
}
}
ポイントはここの緯度経度をViewの座標に変換しているところです。加工した地図はだいたい緯度30 ~ 46、経度124 ~ 150になるようにしてあります。ここの範囲は正方形になっていればいいのでおさまるようにいい感じに調整してください。
private func transformCoordinates(lat: Double, lon: Double, bounds: CGRect) -> CGPoint {
let geoBounds = (minLat: 30.0, maxLat: 46.0, minLon: 124.0, maxLon: 150.0)
let x = (lon - geoBounds.minLon) / (geoBounds.maxLon - geoBounds.minLon) * bounds.width + bounds.origin.x
let y = (1 - (lat - geoBounds.minLat) / (geoBounds.maxLat - geoBounds.minLat)) * bounds.height + bounds.origin.y
return CGPoint(x: x, y: y)
}
あとはJpnMapViewを呼び出すだけです。
import SwiftUI
struct ContentView: View {
var body: some View {
JpnMapView()
}
}
完成![]()
おわりに
地図データを変えてやれば都道府県ごとの地図や標高マップも描けちゃいます![]()
| 大阪府 | 標高マップ |
|---|---|
![]() |
![]() |
そうやって描いた地図を使って作成したアプリがこちらです![]()



