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.
This is the configuration file for the QGIS plugin. It contains metadata such as plugin name, version, icon path, etc.
[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.
importosfromtypingimportOptional,CallablefromPyQt5.QtGuiimportQIconfromPyQt5.QtWidgetsimportQAction,QWidgetfromPyQt5.QtCoreimportQtfrom.ui.config.configimportConfigUifrom.ui.map.mapimportMapUifrom.ui.place.placeimportPlaceUifrom.ui.routes.routesimportRoutesUifrom.ui.terms.termsimportTermsUiclassLocationService:"""
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=ifaceself.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()forcomponentin[self.config,self.map,self.place,self.routes]:component.hide()defadd_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)ifstatus_tipisnotNone:action.setStatusTip(status_tip)ifwhats_thisisnotNone:action.setWhatsThis(whats_this)ifadd_to_menu:self.iface.addPluginToMenu(self.MAIN_NAME,action)ifadd_to_toolbar:self.toolbar.addAction(action)self.actions.append(action)returnactiondefinitGui(self)->None:"""
Initializes the GUI components, adding actions to the interface.
"""components=["config","map","place","routes","terms"]forcomponent_nameincomponents: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,)defunload(self)->None:"""
Cleans up the plugin interface by removing actions and toolbar.
"""foractioninself.actions:self.iface.removePluginMenu(self.MAIN_NAME,action)self.iface.removeToolBarIcon(action)delself.toolbardefshow_config(self)->None:"""
Displays the configuration dialog window.
"""self.config.setWindowFlags(Qt.WindowStaysOnTopHint)# type: ignore
self.config.show()defshow_map(self)->None:"""
Displays the map dialog window.
"""self.map.setWindowFlags(Qt.WindowStaysOnTopHint)# type: ignore
self.map.show()defshow_place(self)->None:"""
Displays the place dialog window.
"""self.place.setWindowFlags(Qt.WindowStaysOnTopHint)# type: ignore
self.place.show()defshow_routes(self)->None:"""
Displays the routes dialog window.
"""self.routes.setWindowFlags(Qt.WindowStaysOnTopHint)# type: ignore
self.routes.show()defshow_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.
This is the UI processing; it loads UI components and displays configuration options.
importosfromPyQt5.QtWidgetsimportQDialog,QMessageBoxfromqgis.PyQtimportuicfrom...utils.configuration_handlerimportConfigurationHandlerfrom...functions.mapimportMapFunctionsclassMapUi(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()exceptExceptionase: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.
fromtypingimportAnyfromqgis.guiimportQgsMapTool,QgsMapCanvas,QgsMapMouseEventfromqgis.coreimport(QgsCoordinateReferenceSystem,QgsProject,QgsCoordinateTransform,QgsPointXY,)classMapClickCoordinateUpdater(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_uiself.active_type=active_typedefcanvasPressEvent(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)defupdate_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),}ifself.active_typeinfield_mapping:lon_field,lat_field=field_mapping[self.active_type]self.set_text_fields(lon_field,lat_field,wgs84_point)defset_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()))deftransform_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())returntransform.transform(map_point)
functions/routes.py
This is the routing function. It creates a line layer in QGIS using the acquired route data.
fromtypingimportDict,Tuple,AnyfromPyQt5.QtCoreimportQVariantfromPyQt5.QtGuiimportQColorfromqgis.coreimport(QgsProject,QgsVectorLayer,QgsFields,QgsField,QgsPointXY,QgsFeature,QgsGeometry,QgsSimpleLineSymbolLayer,QgsSymbol,QgsSingleSymbolRenderer,)from..utils.configuration_handlerimportConfigurationHandlerfrom..utils.external_api_handlerimportExternalApiHandlerclassRoutesFunctions:"""
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.0def__init__(self)->None:"""
Initializes the RoutesFunctions class with configuration and API handlers.
"""self.configuration_handler=ConfigurationHandler()self.api_handler=ExternalApiHandler()defget_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)returnregion,routes,apikeydefcalculate_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)ifresultisNone:raiseValueError("Failed to receive a valid response from the API.")returnresultdefadd_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)defsetup_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)defadd_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()defadd_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=[]forlegindata["Legs"]:line_points=[QgsPointXY(coord[0],coord[1])forcoordinleg["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)defapply_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))
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.