logo
Menu
Amazon Location Service Plugin for QGIS released in OSS

Amazon Location Service Plugin for QGIS released in OSS

I released the QGIS plugin as OSS, utilizing Amazon Location Service, which is probably the world's first!

Published May 27, 2024
Although I have written a QGIS plugin book and released several QGIS plugins in the past, I enjoyed developing QGIS for the first time in a long time.
This is probably the first attempt in the world to develop a QGIS plugin using Amazon Location Service, and I have decided to release this plugin as OSS. This plugin has not yet implemented all the features, but I plan to add more.
Location information technology is being used in a variety of fields. I hope that through this plugin, more people will discover the convenience and potential of the Amazon Location Service. Please give it a try!
In this article, I will introduce how to use this plugin.

Advance Preparation

Building an Amazon Location Service Resources

In advance, build Amazon Location Service resources.
Select from the following to build your resources.
  • AWS Management Console: Manually configure the resource using the GUI.
  • AWS CDK: Automate your infrastructure with code.
  • AWS CloudFormation: Automatically build resources using templates.

How to use plugins

Install QGIS Plugin

Install QGIS plugins. Plugins are registered in the official repositories and can be installed directly from QGIS.
  1. Select "Plugins" → "Manage and Install Plugins..."
  2. Search for "Amazon Location Service"

Menu

Once the plugin is installed, a menu will appear. There are five types of menus: Config, Map, Place, Routes, and Terms.
  • Config: Set each resource name and API key
  • Map: Map display function
  • Place: Geocoding function
  • Routes: Routing function
  • Terms: Display Terms of Use page

Config Function

Configure various settings. Configure region name, API key, Map name, Place name, and Routes name.
  1. Click the “Config” menu
  2. Set each resource name and API key
    • Region: ap-xxxxx
    • API Key: v1.public.xxxxx
    • Map Name: Mapxxxxx
    • Place Name: Placexxxxx
    • Routes Name: Routesxxxxx
  3. Click “Save“

Map Function

This is a map display function. Creates a vector tile layer in QGIS using the acquired vector tiles.
  1. Click the “Map” menu
  2. Select “Map Name“
  3. Click “Add“
  4. The map is displayed as a layer
QGIS does not support all vector tile styles, so some styles may not be displayed.

Place Function

This is a geocoding function. Creates a point layer in QGIS using the acquired address data.
  1. Click the “Place” menu
  2. Select “Select Function“
  3. Click “Get Location“
  4. Click on the location you wish to search
  5. Click “Search”
  6. Search results are displayed in layers

Routes Function

This is a routing function. Create a line layer in QGIS using the acquired route data.
  1. Click the “Routes” menu
  2. Select “Select Function“
  3. Click “Get Location(Starting Point)“
  4. Click the starting point
  5. Click “Get Location(End Point)“
  6. Click on the endpoint
  7. Click “Search”
  8. Search results are displayed in layers

Terms Function

This function displays the Terms of Use.
  1. Click the “Terms” menu
  2. The Terms of Use page will be displayed in your browser.

Plugin Code

The following is a partial code of the plugin.
Overall Configuration
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
location_service/
├── LICENSE
├── __init__.py
├── location_service.py
├── metadata.txt
├── ui/
│ ├── __init__.py
│ ├── icon.png
│ ├── config/
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── config.ui
│ │ ├── config.png
│ └── terms/
│ ├── __init__.py
│ ├── terms.py
│ ├── terms.png
│ ├── terms.ui
│ └── map/
│ ├── __init__.py
│ ├── map.py
│ ├── map.ui
│ ├── map.png
│ └── place/
│ ├── __init__.py
│ ├── place.py
│ ├── place.ui
│ ├── place.png
│ └── routes/
│ ├── __init__.py
│ ├── routes.py
│ ├── routes.ui
│ ├── routes.png
├── utils/
│ ├── __init__.py
│ ├── click_handler.py
│ ├── configuration_handler.py
│ ├── external_api_handler.py
└── functions/
├── __init__.py
├── map.py
├── place.py
├── routes.py

metadata.txt

This is the configuration file for the QGIS plugin. It contains metadata such as plugin name, version, icon path, etc.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[general]
name=Amazon Location Service
description=QGIS Plugin for Amazon Location Service
about=This plugin uses the functionality of Amazon Location Service in QGIS.
qgisMinimumVersion=3.0
version=1.1

#Plugin main icon
icon=ui/icon.png

author=Yasunori Kirimoto
email=info@dayjournal.dev
homepage=https://github.com/dayjournal/qgis-amazonlocationservice-plugin
tracker=https://github.com/dayjournal/qgis-amazonlocationservice-plugin/issues
repository=https://github.com/dayjournal/qgis-amazonlocationservice-plugin
tags=aws,amazonlocationservice,map,geocoding,routing
category=

location_service.py

This is the main process. It initializes the plugin UI and configures various functions.
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
143
import os
from typing import Optional, Callable
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QAction, QWidget
from PyQt5.QtCore import Qt

from .ui.config.config import ConfigUi
from .ui.map.map import MapUi
from .ui.place.place import PlaceUi
from .ui.routes.routes import RoutesUi
from .ui.terms.terms import TermsUi

class LocationService:
"""
Manages the Amazon Location Service interface within a QGIS environment.
"""


MAIN_NAME = "Amazon Location Service"

def __init__(self, iface) -> None:
"""
Initializes the plugin interface, setting up UI components
and internal variables.

Args:
iface (QgsInterface): Reference to the QGIS app interface.
"""

self.iface = iface
self.main_window = self.iface.mainWindow()
self.plugin_directory = os.path.dirname(__file__)
self.actions = []
self.toolbar = self.iface.addToolBar(self.MAIN_NAME)
self.toolbar.setObjectName(self.MAIN_NAME)
self.config = ConfigUi()
self.map = MapUi()
self.place = PlaceUi()
self.routes = RoutesUi()
self.terms = TermsUi()
for component in [self.config, self.map, self.place, self.routes]:
component.hide()

def add_action(
self,
icon_path: str,
text: str,
callback: Callable,
enabled_flag: bool = True,
add_to_menu: bool = True,
add_to_toolbar: bool = True,
status_tip: Optional[str] = None,
whats_this: Optional[str] = None,
parent: Optional[QWidget] = None,
) -> QAction:
"""
Adds an action to the plugin menu and toolbar.

Args:
icon_path (str): Path to the icon.
text (str): Display text.
callback (Callable): Function to call on trigger.
enabled_flag (bool): Is the action enabled by default.
add_to_menu (bool): Should the action be added to the menu.
add_to_toolbar (bool): Should the action be added to the toolbar.
status_tip (Optional[str]): Text for status bar on hover.
whats_this (Optional[str]): Longer description of the action.
parent (Optional[QWidget]): Parent widget.

Returns:
QAction: The created action.
"""

icon = QIcon(icon_path)
action = QAction(icon, text, parent)
action.triggered.connect(callback)
action.setEnabled(enabled_flag)
if status_tip is not None:
action.setStatusTip(status_tip)
if whats_this is not None:
action.setWhatsThis(whats_this)
if add_to_menu:
self.iface.addPluginToMenu(self.MAIN_NAME, action)
if add_to_toolbar:
self.toolbar.addAction(action)
self.actions.append(action)
return action

def initGui(self) -> None:
"""
Initializes the GUI components, adding actions to the interface.
"""

components = ["config", "map", "place", "routes", "terms"]
for component_name in components:
icon_path = os.path.join(
self.plugin_directory, f"ui/{component_name}/{component_name}.png"
)
self.add_action(
icon_path=icon_path,
text=component_name.capitalize(),
callback=getattr(self, f"show_{component_name}"),
parent=self.main_window,
)

def unload(self) -> None:
"""
Cleans up the plugin interface by removing actions and toolbar.
"""

for action in self.actions:
self.iface.removePluginMenu(self.MAIN_NAME, action)
self.iface.removeToolBarIcon(action)
del self.toolbar

def show_config(self) -> None:
"""
Displays the configuration dialog window.
"""

self.config.setWindowFlags(Qt.WindowStaysOnTopHint) # type: ignore
self.config.show()

def show_map(self) -> None:
"""
Displays the map dialog window.
"""

self.map.setWindowFlags(Qt.WindowStaysOnTopHint) # type: ignore
self.map.show()

def show_place(self) -> None:
"""
Displays the place dialog window.
"""

self.place.setWindowFlags(Qt.WindowStaysOnTopHint) # type: ignore
self.place.show()

def show_routes(self) -> None:
"""
Displays the routes dialog window.
"""

self.routes.setWindowFlags(Qt.WindowStaysOnTopHint) # type: ignore
self.routes.show()

def show_terms(self) -> None:
"""
Opens the service terms URL in the default web browser.
"""

self.terms.open_service_terms_url()

ui/map/map.ui

This is the UI file, which defines labels, combo boxes, and buttons in the dialog created by Qt Designer.
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
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>358</width>
<height>166</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>240</width>
<height>0</height>
</size>
</property>
<property name="windowTitle">
<string>Map</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="main_label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:18pt;&quot;&gt;Map&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string/>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="map_label">
<property name="text">
<string>Map Name</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="map_comboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="button_add">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_cancel">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

ui/map/map.py

This is the UI processing; it loads UI components and displays configuration options.
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
import os
from PyQt5.QtWidgets import QDialog, QMessageBox
from qgis.PyQt import uic

from ...utils.configuration_handler import ConfigurationHandler
from ...functions.map import MapFunctions

class MapUi(QDialog):
"""
A dialog for managing map configurations and adding vector tile layers to a
QGIS project.
"""


UI_PATH = os.path.join(os.path.dirname(__file__), "map.ui")
KEY_MAP = "map_value"

def __init__(self) -> None:
"""
Initializes the Map dialog, loads UI components, and populates the map options.
"""

super().__init__()
self.ui = uic.loadUi(self.UI_PATH, self)
self.button_add.clicked.connect(self._add)
self.button_cancel.clicked.connect(self._cancel)
self.map = MapFunctions()
self.configuration_handler = ConfigurationHandler()
self._populate_map_options()

def _populate_map_options(self) -> None:
"""
Populates the map options dropdown with available configurations.
"""

map = self.configuration_handler.get_setting(self.KEY_MAP)
self.map_comboBox.addItem(map)

def _add(self) -> None:
"""
Adds the selected vector tile layer to the QGIS project and closes the dialog.
"""

try:
self.map.add_vector_tile_layer()
self.close()
except Exception as e:
QMessageBox.critical(
self, "Error", f"Failed to add vector tile layer: {str(e)}"
)

def _cancel(self) -> None:
"""
Cancels the operation and closes the dialog without making changes.
"""

self.close()

utils/click_handler.py

This is the map click process. It retrieves the coordinates of the clicked position on the map and reflects them in the specified UI.
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
from typing import Any
from qgis.gui import QgsMapTool, QgsMapCanvas, QgsMapMouseEvent
from qgis.core import (
QgsCoordinateReferenceSystem,
QgsProject,
QgsCoordinateTransform,
QgsPointXY,
)

class MapClickCoordinateUpdater(QgsMapTool):
"""
A tool for updating UI fields with geographic coordinates based on map clicks.
"""


WGS84_CRS = "EPSG:4326"
PLACE_LONGITUDE = "lon_lineEdit"
PLACE_LATITUDE = "lat_lineEdit"
ST_ROUTES_LONGITUDE = "st_lon_lineEdit"
ST_ROUTES_LATITUDE = "st_lat_lineEdit"
ED_ROUTES_LONGITUDE = "ed_lon_lineEdit"
ED_ROUTES_LATITUDE = "ed_lat_lineEdit"

def __init__(self, canvas: QgsMapCanvas, active_ui: Any, active_type: str) -> None:
"""
Initializes the MapClickCoordinateUpdater with a map canvas, UI references,
and the type of coordinates to update.
"""

super().__init__(canvas)
self.active_ui = active_ui
self.active_type = active_type

def canvasPressEvent(self, e: QgsMapMouseEvent) -> None:
"""
Processes mouse press events on the map canvas, converting the click location
to WGS84 coordinates and updating the UI.
"""

map_point = self.toMapCoordinates(e.pos())
wgs84_point = self.transform_to_wgs84(map_point)
self.update_ui(wgs84_point)

def update_ui(self, wgs84_point: QgsPointXY) -> None:
"""
Dynamically updates UI fields designated for longitude and latitude with
new coordinates from map interactions.
"""

field_mapping = {
"st_routes": (self.ST_ROUTES_LONGITUDE, self.ST_ROUTES_LATITUDE),
"ed_routes": (self.ED_ROUTES_LONGITUDE, self.ED_ROUTES_LATITUDE),
"place": (self.PLACE_LONGITUDE, self.PLACE_LATITUDE),
}
if self.active_type in field_mapping:
lon_field, lat_field = field_mapping[self.active_type]
self.set_text_fields(lon_field, lat_field, wgs84_point)

def set_text_fields(
self, lon_field: str, lat_field: str, wgs84_point: QgsPointXY
) -> None:
"""
Helper method to set the text of UI fields designated for longitude and
latitude.
"""

getattr(self.active_ui, lon_field).setText(str(wgs84_point.x()))
getattr(self.active_ui, lat_field).setText(str(wgs84_point.y()))

def transform_to_wgs84(self, map_point: QgsPointXY) -> QgsPointXY:
"""
Converts map coordinates to the WGS84 coordinate system, ensuring global
standardization of the location data.

Args:
map_point (QgsPointXY): A point in the current map's coordinate system
that needs to be standardized.

Returns:
QgsPointXY: The transformed point in WGS84 coordinates, suitable for
global mapping applications.
"""

canvas_crs = QgsProject.instance().crs()
wgs84_crs = QgsCoordinateReferenceSystem(self.WGS84_CRS)
transform = QgsCoordinateTransform(canvas_crs, wgs84_crs, QgsProject.instance())
return transform.transform(map_point)

functions/routes.py

This is the routing function. It creates a line layer in QGIS using the acquired route data.
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
from typing import Dict, Tuple, Any
from PyQt5.QtCore import QVariant
from PyQt5.QtGui import QColor
from qgis.core import (
QgsProject,
QgsVectorLayer,
QgsFields,
QgsField,
QgsPointXY,
QgsFeature,
QgsGeometry,
QgsSimpleLineSymbolLayer,
QgsSymbol,
QgsSingleSymbolRenderer,
)
from ..utils.configuration_handler import ConfigurationHandler
from ..utils.external_api_handler import ExternalApiHandler

class RoutesFunctions:
"""
Manages the calculation and visualization of routes between two points on a map.
"""


KEY_REGION = "region_value"
KEY_ROUTES = "routes_value"
KEY_APIKEY = "apikey_value"
WGS84_CRS = "EPSG:4326"
LAYER_TYPE = "LineString"
FIELD_DISTANCE = "Distance"
FIELD_DURATION = "DurationSec"
LINE_COLOR = QColor(255, 0, 0)
LINE_WIDTH = 2.0

def __init__(self) -> None:
"""
Initializes the RoutesFunctions class with configuration and API handlers.
"""

self.configuration_handler = ConfigurationHandler()
self.api_handler = ExternalApiHandler()

def get_configuration_settings(self) -> Tuple[str, str, str]:
"""
Fetches necessary configuration settings from the settings manager.

Returns:
Tuple[str, str, str]: A tuple containing the region,
route calculator name, and API key.
"""

region = self.configuration_handler.get_setting(self.KEY_REGION)
routes = self.configuration_handler.get_setting(self.KEY_ROUTES)
apikey = self.configuration_handler.get_setting(self.KEY_APIKEY)
return region, routes, apikey

def calculate_route(
self, st_lon: float, st_lat: float, ed_lon: float, ed_lat: float
) -> Dict[str, Any]:
"""
Calculates a route from start to end coordinates using an external API.

Args:
st_lon (float): Longitude of the start position.
st_lat (float): Latitude of the start position.
ed_lon (float): Longitude of the end position.
ed_lat (float): Latitude of the end position.

Returns:
A dictionary containing the calculated route data.
"""

region, routes, apikey = self.get_configuration_settings()
routes_url = (
f"https://routes.geo.{region}.amazonaws.com/routes/v0/calculators/"
f"{routes}/calculate/route?key={apikey}"
)
data = {
"DeparturePosition": [st_lon, st_lat],
"DestinationPosition": [ed_lon, ed_lat],
"IncludeLegGeometry": "true",
}
result = self.api_handler.send_json_post_request(routes_url, data)
if result is None:
raise ValueError("Failed to receive a valid response from the API.")
return result

def add_line_layer(self, data: Dict[str, Any]) -> None:
"""
Adds a line layer to the QGIS project based on route data provided.

Args:
data (Dict): Route data including the route legs and geometry.
"""

routes = self.configuration_handler.get_setting(self.KEY_ROUTES)
layer = QgsVectorLayer(
f"{self.LAYER_TYPE}?crs={self.WGS84_CRS}", routes, "memory"
)
self.setup_layer(layer, data)

def setup_layer(self, layer: QgsVectorLayer, data: Dict[str, Any]) -> None:
"""
Configures the given layer with attributes, features,
and styling based on route data.

Args:
layer (QgsVectorLayer): The vector layer to be configured.
data (Dict): Route data used to populate the layer.
"""

self.add_attributes(layer)
self.add_features(layer, data)
self.apply_layer_style(layer)
layer.triggerRepaint()
QgsProject.instance().addMapLayer(layer)

def add_attributes(self, layer: QgsVectorLayer) -> None:
"""
Adds necessary fields to the vector layer.

Args:
layer (QgsVectorLayer): The layer to which fields are added.
"""

fields = QgsFields()
fields.append(QgsField(self.FIELD_DISTANCE, QVariant.Double))
fields.append(QgsField(self.FIELD_DURATION, QVariant.Int))
layer.dataProvider().addAttributes(fields)
layer.updateFields()

def add_features(self, layer: QgsVectorLayer, data: Dict[str, Any]) -> None:
"""
Adds features to the layer based on the route data.

Args:
layer (QgsVectorLayer): The layer to which features are added.
data (Dict): The route data containing legs and geometry.
"""

features = []
for leg in data["Legs"]:
line_points = [
QgsPointXY(coord[0], coord[1])
for coord in leg["Geometry"]["LineString"]
]
geometry = QgsGeometry.fromPolylineXY(line_points)
feature = QgsFeature(layer.fields())
feature.setGeometry(geometry)
feature.setAttributes([leg["Distance"], leg["DurationSeconds"]])
features.append(feature)
layer.dataProvider().addFeatures(features)

def apply_layer_style(self, layer: QgsVectorLayer) -> None:
"""
Applies styling to the layer to visually differentiate it.

Args:
layer (QgsVectorLayer): The layer to be styled.
"""

symbol_layer = QgsSimpleLineSymbolLayer()
symbol_layer.setColor(self.LINE_COLOR)
symbol_layer.setWidth(self.LINE_WIDTH)
symbol = QgsSymbol.defaultSymbol(layer.geometryType())
symbol.changeSymbolLayer(0, symbol_layer)
layer.setRenderer(QgsSingleSymbolRenderer(symbol))

Terms

Amazon Location Service has terms of use for data usage. Please check the section “82. Amazon Location Service” and use the service at your own risk.
When using HERE as a provider, in addition to the basic terms and conditions, 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.

Comments