Amazon Location Service (以下、Location Service) は地図や場所と緯度軽度の検索、ルート計算などを行うサービスです。そんな Location Service が強化され、トラックなどの配送向けルート計算が可能になりました (公式アナウンス)。今回はそのトラック向けルート計算を使ってルートを描画するだけのシンプルな実装を SwiftUI を使った iOS アプリでなるべくミニマムに実装してみました。

AWS 公式からも iOS 向け Location Service のデモサンプルはあるのですが、Swift に慣れていない私にはちょっと重厚に感じましたので、なるべくミニマムなコードでサンプルを作ってみようと思ったのが今回のモチベーションです。

今回作ってみるアプリ

下のトラックボタン (🚚) を押すと、名古屋駅から京都駅までのルートを描画するだけのシンプルなアプリです。

Screen Shot

今回作成した CDK、Swift コードは GitHub にあげています。

Amazon Location Service のルート計算機能

Location Service のルート計算では移動手段 (車、トラック、スクータ、徒歩) だけでなく、トンネルや有料道路を回避したい、といった回避オプションなども指定して探索できるようになっています (日本の地図でどの程度の精度が出るかは確認が必要だと思います)。

ルート計算で指定できるオプションの詳細はドキュメントも参照ください。ドライバのスケジュール (休憩のサイクル) なんかも指定できるようです。

API ドキュメントも見ていただいても良いかもしれません。

準備

Location Service を利用するにはまず認証が必要になります。認証手段としては API キーや Cognito などが利用できます。今回は地図の表示に API キーを利用し、ルート計算では Cognito の ID プールを利用して匿名ユーザ (認証されていないユーザ) でアクセスしてみます。

ちなみに Location Service はバージョンが新しくなり地図やルートなどのリソースの作成が不要になりました (トラッカー、ジオフェンスの場合は引き続きリソース作成が必要です)。

認証について公式ドキュメントとしては以下あたりに記載があります。

API キーの作成

今回は API キーには地図を表示する権限 (geo-maps:Get*) だけ付与したいと思います。マネジメントコンソールからも作成可能ですが、ここでは CDK で作ってしまいます。以下はその抜粋です。
(keyName は適宜指定してください。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
new cdk.aws_location.CfnAPIKey(this, 'ApiKey', {
  keyName: 'ExampleKey',
  restrictions: {
    allowActions: [
      'geo-maps:GetStaticMap',
      'geo-maps:GetTile',
    ],
    allowResources: [
      `arn:aws:geo-maps:${region}::provider/default`
    ],
  },
  noExpiry: true,
});

CLI で作る場合は以下。 (※ハイライト部分のキーの名前やリージョンは適宜指定してください。リソースの provider/default は固定値です。)

aws location \
  create-key \
  --key-name ExampleKey \
  --restrictions '{
    "AllowActions":[
      "geo-maps:GetStaticMap",
      "geo-maps:GetTile"
    ],
    "AllowResources":[
      "arn:aws:geo-maps:us-east-1::provider/default"
    ]
  }' \
  --no-expiry

作成した API キーは CLI で作成した場合は以下のように、また、マネジメントコンソールや aws location describe-key ... などでも参照できるので Key の値をメモしておきます。

{
  "Key": "v1.public.a1b2c3...",
  "KeyArn": "arn:aws:geo:us-east-1:123456789012:api-key/ExampleKey",
  "KeyName": "ExampleKey",
  "CreateTime": "2024-12-01T09:04:34.937000+00:00"
}

API キーの作成について、公式ドキュメントだと以下になります。

Cognito ID プールの作成

Cognito ID プールの認証されていないユーザ (つまり匿名ユーザ) にルート計算が実行できる権限 (geo-routes:CalculateRoutes) を付与していきます。同じく CDK で作っていきます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// ID プール
const idPool = new cdk.aws_cognito.CfnIdentityPool(this, 'CognitoIdPool', {
  allowClassicFlow: false,
  allowUnauthenticatedIdentities: true,
});

// 認証されていないユーザに付与する権限
const idpUnAuthRole = new cdk.aws_iam.Role(this, 'LocationServiceUnAuthRole', {
  assumedBy: new cdk.aws_iam.FederatedPrincipal('cognito-identity.amazonaws.com', {
    "StringEquals": {
      "cognito-identity.amazonaws.com:aud": idPool.ref
    },
    "ForAnyValue:StringLike": {
      "cognito-identity.amazonaws.com:amr": "unauthenticated"
    }
  },
  'sts:AssumeRoleWithWebIdentity'),
  inlinePolicies: {
    'policy': new cdk.aws_iam.PolicyDocument({
      statements:[
        new cdk.aws_iam.PolicyStatement({
          effect: cdk.aws_iam.Effect.ALLOW,
          actions: [
            'geo-routes:CalculateRoutes',
          ],
          resources: [
            `arn:aws:geo-routes:${region}::provider/default`,
          ],
        }),
      ]
    }),
  },
});

// ID プールに認証されていないユーザ向けのロールを関連付け
new cdk.aws_cognito.CfnIdentityPoolRoleAttachment(this, 'CognitoIdPoolRoleAttachment', {
  identityPoolId: idPool.ref,
  roles: {
    unauthenticated: idpUnAuthRole.roleArn,
  },
});

// 作成した ID プールの ID 確認用
new cdk.CfnOutput(this, 'Output-IdentityPoolId', {
  description: 'Identity Pool ID',
  value: idPool.ref,
});

マネジメントコンソールで作成したい場合は、以下のドキュメントを参照ください。

同じ URL ですが、別のセクションで Location Service の各機能 (地図、場所、ルートなど) に必要な権限も記載されています。

iOS アプリの実装の前の準備

ここからは Xcode を使って、iOS アプリを作っていきます。私自身 iOS アプリをまともに作ったことがないので丁寧に紹介していきたいと思います。

1. プロジェクトの作成

Xcode を起動して “iOS” の “App” を選択して “Next” をクリックします。

Create new project

プロジェクト名などを指定します。今回 “Testing System” はなしにしました。

Specify project name

2. ライブラリの追加 (MapLibre)

アプリ上で地図を表示するためのライブラリとして MapLibre Native SDK for iOS を使用していきます。

プロジェクトのルートで右クリックし、“Add Package Dependencies…” を選択します。

Add package

新しく開かれたウィンドウの右上の検索用テキストボックスに以下の URL を入力します。

Input URL for search text box

画面右側に “MapLibre Native for iOS” が出てきたら右下の “Add Package” をクリックします。

Add MapLibre Native for iOS

以下の画面が出てくるので “Add to Target” を確認して “Add Package” をクリックします。

Add MapLibre Native for iOS to target

正常に追加できると以下のようになります。今回は 6.8.1 というバージョンを利用しました。

Added MapLibre

3. ライブラリの追加 (Amazon Location Service Mobile Authentication SDK for iOS)

続いて、Location Service の API を呼び出す際の認証に利用する SDK を追加します。

MapLibre の時と同様にプロジェクトのルートで右クリックし、“Add Package Dependencies…” を選択し、新しく開かれたウィンドウで以下の URL で検索します。

Add package

画面右側に “Amazon Location Service Mobile Authentication SDK for iOS” が出てきたら右下の “Add Package” をクリックします。

Add Amazon Location Service Mobile Authentication SDK for iOS

以下の画面が出てくるので “Add to Target” を確認して “Add Package” をクリックします。

Add Amazon Location Service Mobile Authentication SDK for iOS to target

正常に追加されると以下のようになります。今回は 1.0.0 というバージョンを利用しました。他に依存するパッケージも自動で追加されます。

Added Location Service Authentication SDK

4. プロパティリスト (Info.plist) の作成

API キーや Cognito ID プールの ID、利用リージョンなどの構成情報をコードに直接記述せず、プロパティリストに定義しておきます。

正しい作り方なのかわからないですが、プロジェクトのルートを選択し、“TARGETS” から選択、“Info” タブを選択後、“Custom iOS Target Properties” の一番下の Key の “+” マークをクリックします。

Create a Property List

レコードが新たに追加されるので Key に名前を設定していきます。

Create a Information Property List

同様の手順で下表のとおりプロパティを作成します。

Key Type Value Example
AmazonCognitoIdentityPoolId String Cognito ID プールの ID <region>:<ランダムな文字列>
AmazonLocationServiceApiKey String API キーの値 v1.public.a1b2c3...
AmazonLocationServiceMapStyle String 地図のスタイル StandardMonochrome など (詳細)
AmazonLocationServiceRegion String 利用リージョン us-east-1

以下のようになっていれば OK です。

Added properties to Property List

iOS アプリの実装

ここから Swift で実装していきます。なるべくミニマムな実装にしていきますので、今回は ContentView.swift ファイルに全て記述していきます。
(あまりお作法とかわかっていない点ご容赦ください 🙇‍♂️)

1. 地図の表示

まず、地図を表示させます。ContentView.swift を以下のようにします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import SwiftUI
import CoreLocation
import MapLibre

struct ContentView: View {
  var body: some View {
    MapView().edgesIgnoringSafeArea(.all)
  }
}

struct MapView: UIViewRepresentable {
  let region: String = Bundle.main.object(forInfoDictionaryKey: "AmazonLocationServiceRegion") as! String
  let style: String = Bundle.main.object(forInfoDictionaryKey: "AmazonLocationServiceMapStyle") as! String
  let apiKey: String = Bundle.main.object(forInfoDictionaryKey: "AmazonLocationServiceApiKey") as! String

  func makeUIView(context: Context) -> MLNMapView {
    let styleURL = URL(string: "https://maps.geo.\(region).amazonaws.com/v2/styles/\(style)/descriptor?color-scheme=Dark&key=\(apiKey)")!
    let mapView = MLNMapView(frame: .zero, styleURL: styleURL)
    mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    mapView.setZoomLevel(11, animated: false)
    mapView.centerCoordinate = CLLocationCoordinate2D(latitude: 35.170099, longitude: 136.880507)

    return mapView
  }

  func updateUIView(_ uiView: MLNMapView, context: Context) {
  }
}

#Preview {
  ContentView()
}

(21行目で指定している緯度経度は名古屋駅の座標です)

MapLibre では SwiftUI に対応していないので UIViewRepresentable プロトコルを実装していきます。MLNMapView クラスに Location Service の Style URL を指定することで View を生成できます。

ここで ▶️ ボタンを押してシミュレータで実行してみます。以下のように表示されれば OK です。

Display map on simulator

2. トラックモードでのルート計算 & ルート描画

続いて、Location Service の ルート計算の API をトラック 🚚 モードで呼び出します。Location Service からは位置情報の配列が含まれた結果が返ってくるのでそれを地図上に描画します。

ContentView.swift の中身を全て、以下のように書き換えます (ハイライト部分は前述から追加した行です)。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
import SwiftUI
import CoreLocation
import MapLibre

import AmazonLocationiOSAuthSDK
import AWSGeoRoutes

struct ContentView: View {

  @State var route: [CLLocationCoordinate2D]?  // ルート計算結果から得た位置情報配列
  @State var isNoRoute: Bool = false           // ルート計算結果が得られたかどうか (エラー表示の判定用)

  var body: some View {
    MapView(route: $route)
      .edgesIgnoringSafeArea(.all)
      .safeAreaInset(edge: .bottom) {
        HStack {
          Spacer()
          Button { // 🚚 ボタン
            Task {
              let origin = [136.884117, 35.170849]      // 出発地: 名古屋駅
              let destination = [135.758783, 34.984068] // 目的地: 京都駅
              route = await getRoute(origin: origin, destination: destination)
              isNoRoute = (route == nil || route!.isEmpty)
            }
          } label: {
            Label("Route", systemImage: "truck.box")
          }
          .buttonStyle(.borderedProminent)
          .padding(.vertical)
          .alert("No route found", isPresented: $isNoRoute){
          } message: {
            Text("Maybe invalid origin or destination. Need to specify points on the road.")
          }
          Spacer()
        }
        .labelStyle(.iconOnly)
        .background(.thinMaterial)
      }
  }
}

struct MapView: UIViewRepresentable {

  let region: String = Bundle.main.object(forInfoDictionaryKey: "AmazonLocationServiceRegion") as! String
  let style: String = Bundle.main.object(forInfoDictionaryKey: "AmazonLocationServiceMapStyle") as! String
  let apiKey: String = Bundle.main.object(forInfoDictionaryKey: "AmazonLocationServiceApiKey") as! String

  @Binding var route: [CLLocationCoordinate2D]?  // ルート計算結果から得た位置情報配列

  func makeUIView(context: Context) -> MLNMapView {
    let styleURL = URL(string: "https://maps.geo.\(region).amazonaws.com/v2/styles/\(style)/descriptor?color-scheme=Dark&key=\(apiKey)")!
    let mapView = MLNMapView(frame: .zero, styleURL: styleURL)
    mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    mapView.setZoomLevel(11, animated: false)
    mapView.centerCoordinate = CLLocationCoordinate2D(latitude: 35.170099, longitude: 136.880507)

    return mapView
  }

  func updateUIView(_ uiView: MLNMapView, context: Context) {
    guard let unwrappedRoute = route, !unwrappedRoute.isEmpty else {
      return
    }

    if let existingAnnotations = uiView.annotations {
      // 描画済みのルートを削除
      uiView.removeAnnotations(existingAnnotations)
    }

    // 新しいルートを描画
    let polyline = MLNPolyline(coordinates: unwrappedRoute, count: UInt(unwrappedRoute.count))
    uiView.addAnnotation(polyline)
  }
}

// ルート計算
func getRoute(origin: [Double], destination: [Double]) async -> [CLLocationCoordinate2D] {
  let cognitoIdentityPoolId = Bundle.main.object(forInfoDictionaryKey: "AmazonCognitoIdentityPoolId") as! String
  let task = Task {
    do {
      // Cognito ID プールの認証されていないユーザの権限で Location Service に接続します
      let authHelper = try await AuthHelper.withIdentityPoolId(identityPoolId: cognitoIdentityPoolId)
      let client = GeoRoutesClient(config: authHelper.getGeoRoutesClientConfig())

      // トラベルモードオプション
      // 今回はトラックモードで全長やタイヤの数、積載物 (ガソリン) などを指定
      // 詳細: https://docs.aws.amazon.com/location/latest/APIReference/API_RouteTruckOptions.html
      let travelModeOptions = GeoRoutesClientTypes.RouteTravelModeOptions(
        truck: GeoRoutesClientTypes.RouteTruckOptions(
          axleCount: 4,                      // 車軸
          engineType: .internalCombustion,   // エンジンタイプ (他に EV、PHV)
          grossWeight: 10000,                // 総重量 (kg)
          hazardousCargos: [ .gas ],         // 危険貨物
          height: 280,                       // 高さ (cm)
          length: 1200,                      // 長さ (cm)
          maxSpeed: 80,                      // 最大速度 (km/h)
          occupancy: 2,                      // 乗員数
          payloadCapacity: 9000,             // 積載量 (kg)
          tireCount: 8,                      // タイヤの数
          truckType: .straightTruck,         // トラックのタイプ
          width: 250                         // 幅 (cm)
        )
      )

      // ルート計算 API の入力パラメータ
      let input = AWSGeoRoutes.CalculateRoutesInput(
        // 回避オプション有料道路、トンネルを回避するよう指定
        // 詳細: https://docs.aws.amazon.com/location/latest/APIReference/API_RouteAvoidanceOptions.html
        avoid: GeoRoutesClientTypes.RouteAvoidanceOptions(tollRoads: true, tunnels: true),
        destination: destination,
        legGeometryFormat: .simple,
        origin: origin,
        travelMode: .truck,
        travelModeOptions: travelModeOptions
      )

      // ルート計算実行
      let output = try await client.calculateRoutes(input: input)

      // ルート計算結果から `CLLocationCoordinate2D` の配列を生成
      var routePoints: [CLLocationCoordinate2D] = []
      output.routes?.forEach { route in
        route.legs?.forEach { leg in
          leg.geometry?.lineString?.forEach { point in
            routePoints.append(CLLocationCoordinate2D(latitude: point[1], longitude: point[0]))
          }
        }
      }
      return routePoints
    } catch {
      print(error.localizedDescription)
    }
    return []
  }

  return await task.value
}

#Preview {
  ContentView()
}

少し解説

ルート計算部分

Location Service の API を呼び出すクライアントは以下のコードで生成できます。
今回は Cognito ID プールを使用しました。API キーを使いたい場合は GitHub で紹介されているのでそちらを参照ください。

82
83
84
// Cognito ID プールの認証されていないユーザの権限で Location Service に接続します
let authHelper = try await AuthHelper.withIdentityPoolId(identityPoolId: cognitoIdentityPoolId)
let client = GeoRoutesClient(config: authHelper.getGeoRoutesClientConfig())

トラベルモードでトラックを選択するといろいろなオプションを指定できます。
ここでは指定しなかったオプションもあるので詳しくは API ドキュメントを参照ください。

 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
let travelModeOptions = GeoRoutesClientTypes.RouteTravelModeOptions(
  truck: GeoRoutesClientTypes.RouteTruckOptions(
    axleCount: 4,                      // 車軸
    engineType: .internalCombustion,   // エンジンタイプ (他に EV、PHV)
    grossWeight: 10000,                // 総重量 (kg)
    hazardousCargos: [ .gas ],         // 危険貨物
    height: 280,                       // 高さ (cm)
    length: 1200,                      // 長さ (cm)
    maxSpeed: 80,                      // 最大速度 (km/h)
    occupancy: 2,                      // 乗員数
    payloadCapacity: 9000,             // 積載量 (kg)
    tireCount: 8,                      // タイヤの数
    truckType: .straightTruck,         // トラックのタイプ
    width: 250                         // 幅 (cm)
  )
)

トラックの他には以下があります。

API への入力値で RouteAvoidanceOptions を指定することで回避オプションを設定できます (ハイライト部分)。 オプションは他にもあり、エリアの指定、フェリー、U ターン…などいろいろあります。詳細は API ドキュメントも参照ください。

106
107
108
109
110
111
112
113
114
115
116
// ルート計算 API の入力パラメータ
let input = AWSGeoRoutes.CalculateRoutesInput(
  // 回避オプション有料道路、トンネルを回避するよう指定
  // 詳細: https://docs.aws.amazon.com/location/latest/APIReference/API_RouteAvoidanceOptions.html
  avoid: GeoRoutesClientTypes.RouteAvoidanceOptions(tollRoads: true, tunnels: true),
  destination: destination,
  legGeometryFormat: .simple,
  origin: origin,
  travelMode: .truck,
  travelModeOptions: travelModeOptions
)

ルート計算の API 呼び出しは以下になります。

118
119
// ルート計算実行
let output = try await client.calculateRoutes(input: input)

ルート描画部分

メインの View にルート計算結果から得られる経路のポイントの配列 ([CLLocationCoordinate2D]) を持っておき、配列が更新されたら MapView が更新されるように @Binding を使って連携します。

経路の線は MLNPolyline を使って生成し、MapView の addAnnotation() を使って描画しています。

72
73
let polyline = MLNPolyline(coordinates: unwrappedRoute, count: UInt(unwrappedRoute.count))
uiView.addAnnotation(polyline)

実行結果

以下のようなオプション (抜粋) でルート計算を実行してみました。

  • 97行目 : 最高速度 80 km/h
  • 110行目 : 有料道路とトンネルを避ける

シミュレータで実行し🚚ボタンを押すと以下のように青い線でルートが描画されます。

Result of Route Calculate with option1

それではオプションを以下のように変えて実行してみます。

  • 最高速度 100 km/h
    • 97行目: maxSpeed: 100,
  • 有料道路、トンネルとも避けない
    • 110行目: tollRoads: false, tunnels: false

実行結果は以下のようになりました。かなりルートが変わりました。

Result of Route Calculate with option2

指定できるオプションは他にもいろいろあるので詳細はドキュメントを参照ください。

SDK のドキュメントは以下

CalculateRoutes 実行時の注意点

今回は出発地 (origin)、目的地 (destination)をそれぞれ名古屋駅、京都駅で決めうちで位置情報を指定して、CalculateRoutes を実行しました。ここで指定するそれぞれの座標は道路を示す位置情報である必要がある点ご注意ください。

道路を示す位置情報が渡されなかった場合 (建物などの場合)、エラーが返ってきます。

後片付け

AWS 側のリソースを CDK で作った場合、cdk destroy では API キーが削除できずに失敗してしまいます。

以下のコマンドで API キーを削除してから、cdk destroy を実行します。

aws location delete-key \
  --key-name ${API_KEY_NAME}
  --force-delete

MapKit は使えないの?

iOS には MapKit があり、簡単に地図などを扱うことができます。一方 Location Service のようにトラック用ルート計算といったことはできないので、ルート計算を Location Service で実行しつつ、経路を MapKit の地図に描画する、といった方法も考えられそうです。

しかし、AWS の Service Term (サービス条件) の 82.5. に以下のように記載があります。

82.5. In addition to the restrictions in Section 82.4, if you use HERE as your Geolocation Provider, you may not:
 a. Store or cache any Location Data for Japan, including any geocoding or reverse-geocoding results.
 b. Layer routes from HERE on top of a map from another third-party provider, or layer routes from another third-party provider on top of maps from HERE.

今回使用したルート計算はどのジオロケーションプロバイダのものを使用したのか分かりにくいですが、Location Service 製品サイトの HERE Technologies の紹介を見ると、トラック用のルート計算などを提供していることから HERE の機能を使っていそうにも見えます。

MapKit の地図上に、今回のルート計算結果を描画することは、上記サービス条件の 82.5. b. の禁止事項に該当しそうな気がします。

※私は法務の専門家ではないので正確なアドバイスはできません。ご利用の際に各自適切にご判断ください。

まとめ

Amazon Location Service の新しい機能として、回避オプションを指定したトラック用のルート計算などが可能になりました。今回はこの機能を使って、とっつきやすい最小限のサンプルを iOS 向けに Swift で実装してみました。Location Service を使って iOS アプリを作ってみるきっかけになれば幸いです。

今回作成した CDK、Swift コードは以下 GitHub にあります。

最後に・・・

この投稿は個人的なものであり、所属組織を代表するものではありません。ご了承ください。