├── .DS_Store ├── .eslintrc.js ├── .gitignore ├── .graphqlconfig.yml ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── docs └── deployment_guide.md ├── images ├── architecture.jpg ├── architecture.png ├── geotrack-agent.png ├── geotrack-delivery.png ├── geotrack-home.png ├── home.png └── map.png ├── lambdas ├── eventbridge │ └── index.py ├── iot │ └── index.py └── simulation │ ├── launchDeliveryFleet │ └── index.py │ └── pushVehiclePosition │ └── index.py ├── layers └── requests │ └── requirements.txt ├── package-lock.json ├── public ├── favicon.ico └── index.html ├── schema.graphql ├── template.yaml ├── webapp ├── .gitignore ├── .vscode │ └── extensions.json ├── index.html ├── package-lock.json ├── package.json ├── public │ └── favicon.ico ├── src │ ├── App.vue │ ├── assets │ │ ├── base.css │ │ ├── logo.svg │ │ └── main.css │ ├── components │ │ ├── Header.vue │ │ └── Map.vue │ ├── configAmplify.js │ ├── graphql │ │ ├── mutations.js │ │ ├── queries.js │ │ └── subscriptions.js │ ├── layouts │ │ └── SimpleLayout.vue │ ├── main.js │ ├── resolvers │ │ ├── addDriverTrip.js │ │ ├── delById.js │ │ ├── getById.js │ │ ├── hydrateTrips.js │ │ ├── listDrivers.js │ │ ├── listTrips.js │ │ ├── removeDriverTrip.js │ │ ├── saveById.js │ │ ├── statusTrips.js │ │ └── updateById.js │ ├── router │ │ └── index.js │ ├── stores │ │ ├── geo.js │ │ └── user.js │ └── views │ │ ├── AboutView.vue │ │ ├── AuthView.vue │ │ ├── DriversView.vue │ │ ├── HomeView.vue │ │ └── TripsView.vue └── vite.config.js └── webappconfig.sh /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-location-service-geotrack-vuejs/400380be7c834b58de69c70600a7fdd83d0303f5/.DS_Store -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 16 | } 17 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #amplify-do-not-edit-begin 2 | amplify/\#current-cloud-backend 3 | amplify/.config/local-* 4 | amplify/logs 5 | amplify/mock-data 6 | amplify/backend/amplify-meta.json 7 | amplify/backend/.temp 8 | build/ 9 | dist/ 10 | node_modules/ 11 | aws-exports.js 12 | awsconfiguration.json 13 | amplifyconfiguration.json 14 | amplifyconfiguration.dart 15 | amplify-build-config.json 16 | amplify-gradle-config.json 17 | amplifytools.xcconfig 18 | .secret-* 19 | **.sample 20 | #amplify-do-not-edit-end 21 | amplify/team-provider-info.json 22 | .secret-* 23 | samconfig.toml 24 | queries.txt 25 | .env* 26 | .aws-sam -------------------------------------------------------------------------------- /.graphqlconfig.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | geotrack: 3 | schemaPath: src/graphql/schema.json 4 | includes: 5 | - src/graphql/**/*.js 6 | excludes: 7 | - ./amplify/** 8 | extensions: 9 | amplify: 10 | codeGenTarget: javascript 11 | generatedFileName: '' 12 | docsFilePath: src/graphql 13 | extensions: 14 | amplify: 15 | version: 3 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "amplify/.config": true, 4 | "amplify/**/*-parameters.json": true, 5 | "amplify/**/amplify.state": true, 6 | "amplify/**/transform.conf.json": true, 7 | "amplify/#current-cloud-backend": true, 8 | "amplify/backend/amplify-meta.json": true, 9 | "amplify/backend/awscloudformation": true 10 | } 11 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Amazon Location Service GeoTrack Vue.js 2 | 3 | Location data is a vital ingredient in today's applications, enabling capabilities ranging from asset tracking to location-based marketing. 4 | 5 | With [Amazon Location Service](https://aws.amazon.com/location/), you can easily add capabilities such as maps, points of interest, geocoding, routing, geofences, and tracking to applications. You retain control of your location data with Amazon Location, so you can combine proprietary data with data from the service. Amazon Location provides cost-effective location-based services (LBS) using high-quality data from global, trusted providers Esri and HERE Technologies. 6 | 7 | This repo contains a Vue.js prototype that controls a delivery system. For the prototype to work you need to first create the agents and associate unique IoT device Ids to them. Once you have the delivery agents in the system, you can go add the routes they need to go. The form leverages Amazon Location Maps to display de map, Places to fing the latitute and longitute associated to the address typed, Geogence to define a perimeter at the destination so the person can receive a text message when the driver is near by, and Routes to calculate the estimated time and distance. 8 | 9 | At the toolbar there is a fire icon button. Upon clicking this button, the application will simulate the existent delivery routes. An AWS Lambda reads the start and end positions of each delivery route, calculates the route and sends IoT messages with the IoT devices associated to the delivery agents reporting their geo-location over time. The application does not prevent having two routes with the same IoT device, which will produce inconsistent position. 10 | 11 | ## Architecture Overview 12 | 13 | 14 | 15 | ## Stack 16 | 17 | * **Front-end** - Vue.js as the core framework, [Vuetify](https://vuetifyjs.com/en/) for UI, [MapLibre](https://github.com/maplibre) for map visualiztion, [AWS Amplify](https://aws.amazon.com/amplify/) libraries for Auth UI component and AWS integration. 18 | * **Data** - User data is saved in [Amazon DynamoDB](https://aws.amazon.com/dynamodb/) via GraphQL using [AWS AppSync](https://aws.amazon.com/appsync/). Devices GPS positions are stored in Amazon Location Service Tracker. 19 | * **Auth** - [Amazon Cognito](https://aws.amazon.com/cognito/) provides JSON Web Tokens (JWT) and along with AppSync fine-grained authorization on what data types users can access. 20 | * **IoT** - [AWS IoT](https://aws.amazon.com/iot/) with topics and rules. 21 | * **Serverless** - [AWS Lambda](https://aws.amazon.com/lambda/) for backend processes. 22 | 23 | ## User Interface 24 | 25 | #### Real-time tracking visualization 26 | 27 | 28 | #### Managing Delivery Agents 29 | 30 | 31 | #### Managing Delivery Routes 32 | 33 | 34 | # Deployment 35 | To deploy this solution into your AWS Account please follow our [Deployment Guide](./docs/deployment_guide.md) 36 | 37 | ## Security 38 | 39 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 40 | 41 | ## License 42 | 43 | This library is licensed under the MIT-0 License. See the LICENSE file. 44 | 45 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/deployment_guide.md: -------------------------------------------------------------------------------- 1 | # Requirements 2 | Before you deploy, you must have the following in place: 3 | * [AWS Account](https://aws.amazon.com/account/) 4 | * [GitHub Account](https://github.com/) 5 | * [AWS CLI](https://aws.amazon.com/cli/) 6 | * [AWS SAM](https://aws.amazon.com/serverless/sam/) 7 | * Python 3.9, NodeJs v18 and npm 8 | 9 | 10 | 11 | # Step 1: Deploy the Solution 12 | 13 | In this step we deploy all resources this solution requires, including AWS AppSync Schema and its resolvers, via AWS SAM. 14 | 15 | The first step is to execute **sam build** so the solution can prepare all the Python libraries to be installed. 16 | 17 | ```bash 18 | sam build 19 | ``` 20 | 21 | Next step is to deploy the solution 22 | 23 | ```bash 24 | sam deploy -g --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND CAPABILITY_NAMED_IAM 25 | ``` 26 | 27 | In the questions, provide the stack-name as geotrack-backend the region you are deploying the solution and accept all the default options. The output should be similar to: 28 | 29 | ```bash 30 | Setting default arguments for 'sam deploy' 31 | ========================================= 32 | Stack Name [geotrack-v2]: 33 | AWS Region [us-west-2]: 34 | Parameter ProjectName [geotrack]: 35 | Parameter EnvironmentName [dev]: 36 | #Shows you resources changes to be deployed and require a 'Y' to initiate deploy 37 | Confirm changes before deploy [Y/n]: y 38 | #SAM needs permission to be able to create roles to connect to the resources in your template 39 | Allow SAM CLI IAM role creation [Y/n]: y 40 | #Preserves the state of previously provisioned resources when an operation fails 41 | Disable rollback [y/N]: n 42 | Save arguments to configuration file [Y/n]: y 43 | SAM configuration file [samconfig.toml]: 44 | SAM configuration environment [default]: 45 | ``` 46 | 47 | Confirm the deploy of the changeset and wait for the to finish. 48 | 49 | # Step 2: Deploy the WebApp 50 | 51 | ```bash 52 | ./webappconfig.sh 53 | ``` -------------------------------------------------------------------------------- /images/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-location-service-geotrack-vuejs/400380be7c834b58de69c70600a7fdd83d0303f5/images/architecture.jpg -------------------------------------------------------------------------------- /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-location-service-geotrack-vuejs/400380be7c834b58de69c70600a7fdd83d0303f5/images/architecture.png -------------------------------------------------------------------------------- /images/geotrack-agent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-location-service-geotrack-vuejs/400380be7c834b58de69c70600a7fdd83d0303f5/images/geotrack-agent.png -------------------------------------------------------------------------------- /images/geotrack-delivery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-location-service-geotrack-vuejs/400380be7c834b58de69c70600a7fdd83d0303f5/images/geotrack-delivery.png -------------------------------------------------------------------------------- /images/geotrack-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-location-service-geotrack-vuejs/400380be7c834b58de69c70600a7fdd83d0303f5/images/geotrack-home.png -------------------------------------------------------------------------------- /images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-location-service-geotrack-vuejs/400380be7c834b58de69c70600a7fdd83d0303f5/images/home.png -------------------------------------------------------------------------------- /images/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-location-service-geotrack-vuejs/400380be7c834b58de69c70600a7fdd83d0303f5/images/map.png -------------------------------------------------------------------------------- /lambdas/eventbridge/index.py: -------------------------------------------------------------------------------- 1 | from urllib import request 2 | from datetime import datetime 3 | import os 4 | import json 5 | import boto3 6 | import logging 7 | import requests 8 | from requests_aws4auth import AWS4Auth 9 | 10 | logger = logging.getLogger() 11 | logger.setLevel(logging.INFO) 12 | 13 | pinpoint = boto3.client('pinpoint') 14 | appsync = boto3.client('appsync') 15 | 16 | appsync_url = os.getenv('APPSYNC_URL') 17 | project_name = os.getenv('PROJECT_NAME') 18 | project_env = os.getenv('PROJECT_ENV') 19 | pinpoint_application_id = os.getenv('APPLICATION_ID') 20 | 21 | boto3_session = boto3.Session() 22 | credentials = boto3_session.get_credentials() 23 | credentials = credentials.get_frozen_credentials() 24 | 25 | auth = AWS4Auth( 26 | credentials.access_key, 27 | credentials.secret_key, 28 | boto3_session.region_name, 29 | 'appsync', 30 | session_token=credentials.token, 31 | ) 32 | 33 | def getGeoFenceRecord(geoFenceId): 34 | graphqlQuery=""" 35 | query listDeliveryInfos { 36 | listDeliveryInfos(filter: { and: [ 37 | { status: { ne: "completed" }}, 38 | { geoFenceId: { eq: "%s" }} ]} 39 | ) { 40 | items { 41 | id 42 | geoFenceId 43 | userPhone 44 | status 45 | deliveryAgent { 46 | id 47 | fullName 48 | device 49 | { 50 | id 51 | } 52 | } 53 | } 54 | } 55 | } 56 | """%(geoFenceId) 57 | 58 | session = requests.Session() 59 | session.auth = auth 60 | 61 | response = session.request( 62 | url=appsync_url, 63 | method='POST', 64 | json={'query': graphqlQuery} 65 | ) 66 | 67 | print(response.json()) 68 | 69 | if 'data' in response.json() and len(response.json()['data']['listDeliveryInfos']['items']) == 1: 70 | return response.json()['data']['listDeliveryInfos']['items'][0] 71 | else: 72 | logger.error(response) 73 | return [] 74 | 75 | def handler(event, context): 76 | # event - Data from EventBridge 77 | # print(event) 78 | 79 | row = getGeoFenceRecord(event['detail']['GeofenceId']) 80 | if 'deliveryAgent' in row: 81 | deviceId = row['deliveryAgent']['device']['id'] 82 | 83 | if event['detail']['DeviceId'] == deviceId: 84 | response = pinpoint.send_messages( 85 | ApplicationId=pinpoint_application_id, 86 | MessageRequest={ 87 | 'Addresses': { 88 | 'string': { 89 | 'ChannelType': 'SMS', 90 | 'RawContent': 'string' 91 | } 92 | }, 93 | 'MessageConfiguration': { 94 | 'SMSMessage': { 95 | 'Body': 'The driver should be arriving soon', 96 | 'Keyword': 'GeoTrack', 97 | 'MessageType': 'TRANSACTIONAL' 98 | }, 99 | }, 100 | 'TraceId': 'string' 101 | } 102 | ) 103 | 104 | print(response) 105 | 106 | return { 107 | 'statusCode': 200, 108 | 'body': json.dumps('Success') 109 | } 110 | else: 111 | return { 112 | 'statusCode': 500, 113 | 'body': json.dumps('Error') 114 | } 115 | 116 | -------------------------------------------------------------------------------- /lambdas/iot/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import boto3 4 | import logging 5 | 6 | logger = logging.getLogger() 7 | logger.setLevel(logging.INFO) 8 | 9 | project_name = os.getenv('PROJECT_NAME') 10 | project_env = os.getenv('PROJECT_ENV') 11 | TRACKER = os.getenv('TRACKER') 12 | location = boto3.client('location') 13 | 14 | def handler(event, context): 15 | 16 | response = location.batch_update_device_position( 17 | TrackerName=TRACKER, 18 | Updates=[ 19 | { 20 | 'DeviceId': event['device_id'], 21 | 'Position': [ 22 | event['longitude'], event['latitude'] 23 | ], 24 | 'SampleTime': event['timestamp'] 25 | }, 26 | ] 27 | ) 28 | 29 | return { 30 | 'statusCode': 200, 31 | 'body': json.dumps(response) 32 | } 33 | -------------------------------------------------------------------------------- /lambdas/simulation/launchDeliveryFleet/index.py: -------------------------------------------------------------------------------- 1 | from urllib import request 2 | from datetime import datetime 3 | import os 4 | import json 5 | import boto3 6 | import logging 7 | import requests 8 | from requests_aws4auth import AWS4Auth 9 | 10 | logger = logging.getLogger() 11 | logger.setLevel(logging.INFO) 12 | 13 | lambda_client = boto3.client('lambda') 14 | appsync = boto3.client('appsync') 15 | 16 | appsync_url = os.getenv('APPSYNC_URL') 17 | project_name = os.getenv('PROJECT_NAME') 18 | project_env = os.getenv('PROJECT_ENC') 19 | pushVehicleLambda = os.getenv('PUSH_VEHICLE_LAMBDA_NAME') 20 | 21 | boto3_session = boto3.Session() 22 | credentials = boto3_session.get_credentials() 23 | credentials = credentials.get_frozen_credentials() 24 | items = [] 25 | 26 | auth = AWS4Auth( 27 | credentials.access_key, 28 | credentials.secret_key, 29 | boto3_session.region_name, 30 | 'appsync', 31 | session_token=credentials.token, 32 | ) 33 | 34 | graphqlQuery="""" 35 | query DeviceIdByTripStatus ( 36 | $status: TripStatus 37 | ) { 38 | statusTrips(status: $status) { 39 | nextToken 40 | trips { 41 | id 42 | geoStart { 43 | lat 44 | lng 45 | } 46 | geoEnd { 47 | lat 48 | lng 49 | } 50 | duration 51 | distance 52 | status 53 | driver { 54 | fullName 55 | deviceId 56 | } 57 | } 58 | } 59 | } 60 | """ 61 | 62 | def setProxyResponse(data): 63 | response = {} 64 | response["isBase64Encoded"] = False 65 | if "statusCode" in data: 66 | response["statusCode"] = data["statusCode"] 67 | else: 68 | response["statusCode"] = 200 69 | if "headers" in data: 70 | response["headers"] = data["headers"] 71 | else: 72 | response["headers"] = { 73 | 'Content-Type': 'application/json', 74 | 'Access-Control-Allow-Origin': '*' 75 | } 76 | response["body"] = json.dumps(data["body"]) 77 | return response 78 | 79 | def handler(event, context): 80 | 81 | proxy_response = {} 82 | 83 | session = requests.Session() 84 | session.auth = auth 85 | 86 | response = session.request( 87 | url=appsync_url, 88 | method='POST', 89 | json={'query': graphqlQuery} 90 | ) 91 | 92 | print(response.json()) 93 | 94 | if 'data' in response.json(): 95 | items = response.json()['data']['statusTrips']['trips'] 96 | for row in items: 97 | 98 | response = lambda_client.invoke( 99 | FunctionName=str(pushVehicleLambda), 100 | InvocationType='Event', 101 | Payload=json.dumps(row) 102 | ) 103 | 104 | proxy_response['statusCode']=200 105 | proxy_response["body"] = { 'msg': 'Processed ' + str(len(items)) + ' vehicles' } 106 | 107 | return setProxyResponse(proxy_response) -------------------------------------------------------------------------------- /lambdas/simulation/pushVehiclePosition/index.py: -------------------------------------------------------------------------------- 1 | import random 2 | import uuid 3 | import os 4 | import sys 5 | import base64 6 | import json 7 | import urllib 8 | import logging 9 | import boto3 10 | from boto3.dynamodb.conditions import Key, Attr 11 | from time import sleep 12 | from datetime import datetime 13 | 14 | logger = logging.getLogger() 15 | logger.setLevel(logging.INFO) 16 | 17 | iot_topic = os.getenv('IOT_TOPIC') 18 | project_name = os.getenv('PROJECT_NAME') 19 | project_env = os.getenv('PROJECT_ENV') 20 | ROUTE_NAME = os.getenv('ROUTE_NAME') 21 | TRACKER_NAME = os.getenv('TRACKER_NAME') 22 | 23 | location = boto3.client('location') 24 | iot = boto3.client('iot-data') 25 | 26 | def route_calculation(departure, destination): 27 | return location.calculate_route( 28 | CalculatorName=ROUTE_NAME, 29 | DeparturePosition=[departure['lng'], departure['lat']], 30 | DestinationPosition=[destination['lng'], destination['lat']], 31 | DepartNow=True, 32 | DistanceUnit='Kilometers', 33 | TravelMode='Car', 34 | ) 35 | 36 | def publish_location(trip_id, device_id, position): 37 | logger.info("Publishing device: " + str(device_id) + " at lng:" + str(position[0]) + " lat:" + str(position[1])) 38 | message = json.dumps( 39 | { 40 | "TrackerName": TRACKER_NAME, 41 | "DeviceID" :device_id, 42 | "position": [ 43 | float(position[1]), 44 | float(position[0]) 45 | ], 46 | "timestamp": datetime.now().isoformat(), 47 | "tripId": trip_id 48 | }) 49 | 50 | try: 51 | iot.publish( 52 | topic=iot_topic, 53 | qos=0, 54 | payload=message 55 | ) 56 | 57 | except iot.exceptions.InternalFailureException as e: 58 | logger.error("Location InternalFailureException function error: " + str(e)) 59 | except iot.exceptions.InvalidRequestException as e: 60 | logger.error("Location InvalidRequestException function error: " + str(e)) 61 | except iot.exceptions.UnauthorizedException as e: 62 | logger.error("Location UnauthorizedException function error: " + str(e)) 63 | except iot.exceptions.MethodNotAllowedException as e: 64 | logger.error("Location MethodNotAllowedException function error: " + str(e)) 65 | except Exception as e: 66 | logger.error(str(e)) 67 | 68 | 69 | def get_random(min, max): 70 | num = round(random.uniform(min, max), 2) 71 | if (num.is_integer()): 72 | return num + 0.01 73 | else: 74 | return num 75 | 76 | def handler(event, context): 77 | print(event) 78 | route = route_calculation(event['geoStart'],event['geoEnd']) 79 | if 'Legs' in route: 80 | for step in route['Legs'][0]['Steps']: 81 | publish_location(event['id'], event['driver']['deviceId'], step['StartPosition']) 82 | if step['DurationSeconds'] >= 200: 83 | div=10 84 | elif step['DurationSeconds'] < 200 and step['DurationSeconds'] >= 100: 85 | div=6 86 | else: 87 | div=4 88 | 89 | logger.info("Sleeping: " + str(round(step['DurationSeconds']/div)) + " sec") 90 | sleep(round(step['DurationSeconds']/div)) 91 | publish_location(event['id'], event['driver']['deviceId'], step['EndPosition']) 92 | 93 | 94 | response = { 95 | 'statusCode': 200, 96 | 'body': 'successfully read items!' 97 | } 98 | 99 | return response -------------------------------------------------------------------------------- /layers/requests/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.32.2 2 | requests_aws4auth 3 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-location-service-geotrack-vuejs", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-location-service-geotrack-vuejs/400380be7c834b58de69c70600a7fdd83d0303f5/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 12 | 15 |
16 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | type Coordinate @aws_iam @aws_cognito_user_pools { 2 | lat: Float 3 | lng: Float 4 | } 5 | 6 | input CoordinateInput { 7 | lat: Float 8 | lng: Float 9 | } 10 | 11 | type Driver @aws_iam @aws_cognito_user_pools { 12 | id: ID 13 | email: AWSEmail 14 | fullName: String 15 | deliveryType: String 16 | deviceId: String 17 | deviceType: String 18 | status: DriverStatus 19 | createdAt: AWSDateTime 20 | updatedAt: AWSDateTime 21 | trips: [Trip] 22 | } 23 | 24 | input DriverInput { 25 | id: ID 26 | email: AWSEmail 27 | fullName: String 28 | deliveryType: String 29 | deviceId: String 30 | deviceType: String 31 | status: DriverStatus 32 | createdAt: AWSDateTime 33 | updatedAt: AWSDateTime 34 | trips: [TripInput] 35 | } 36 | 37 | enum DriverStatus { 38 | active 39 | inactive 40 | banned 41 | } 42 | 43 | type PaginatedDrivers { 44 | drivers: [Driver!]! 45 | nextToken: String 46 | } 47 | 48 | type PaginatedTrips @aws_iam 49 | @aws_cognito_user_pools { 50 | trips: [Trip!]! 51 | nextToken: String 52 | } 53 | 54 | type Trip @aws_iam 55 | @aws_cognito_user_pools { 56 | id: ID 57 | driver: Driver 58 | labelStart: String 59 | geoStart: Coordinate 60 | labelEnd: String 61 | geoEnd: Coordinate 62 | duration: Float 63 | distance: Float 64 | geoFenceId: ID 65 | clientphone: String 66 | expireAt: Float 67 | status: TripStatus 68 | createdAt: AWSDateTime 69 | updatedAt: AWSDateTime 70 | } 71 | 72 | input TripInput { 73 | id: ID 74 | driver: DriverInput 75 | geoStart: CoordinateInput 76 | geoEnd: CoordinateInput 77 | labelStart: String 78 | labelEnd: String 79 | duration: Float 80 | distance: Float 81 | geoFenceId: ID 82 | clientPhone: String 83 | expireAt: Float 84 | status: TripStatus 85 | createdAt: AWSTimestamp 86 | updatedAt: AWSTimestamp 87 | } 88 | 89 | enum TripStatus { 90 | completed 91 | inroute 92 | accepted 93 | error 94 | onhold 95 | } 96 | 97 | type Mutation { 98 | saveDriver(input: DriverInput): Driver 99 | delDriver(id: ID!): Driver 100 | saveTrip(input: TripInput): Trip 101 | delTrip(id: ID!): Trip 102 | } 103 | 104 | type Query { 105 | getDriver(id: ID!): Driver 106 | listDrivers(limit: Int, nextToken: String): PaginatedDrivers 107 | addDriverTrip(driverId: ID!, tripId: ID!): Driver 108 | removeDriverTrip(driverId: ID!, tripId: ID!): Driver 109 | getTrip(id: ID!): Trip 110 | listTrips(limit: Int, nextToken: String): PaginatedTrips 111 | statusTrips(status: TripStatus, limit: Int, nextToken: String): PaginatedTrips 112 | @aws_iam 113 | @aws_cognito_user_pools 114 | } 115 | 116 | schema { 117 | query: Query 118 | mutation: Mutation 119 | } -------------------------------------------------------------------------------- /template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | "GeoTrack Application v2\n" 5 | Globals: 6 | Function: 7 | AutoPublishAlias: live 8 | Handler: index.handler 9 | MemorySize: 256 10 | Runtime: python3.10 11 | Timeout: 120 12 | Tracing: Active 13 | 14 | Parameters: 15 | ProjectName: 16 | Type: String 17 | Default: geotrack 18 | Description: A description to identify project 19 | EnvironmentName: 20 | Type: String 21 | Default: dev 22 | Description: A description to identify environment (e.g. dev, prod) 23 | 24 | Resources: 25 | CognitoUserPool: 26 | Type: AWS::Cognito::UserPool 27 | Properties: 28 | UserPoolName: !Sub ${ProjectName}-${EnvironmentName}-userpool 29 | Policies: 30 | PasswordPolicy: 31 | MinimumLength: 8 32 | UsernameAttributes: 33 | - email 34 | AutoVerifiedAttributes: 35 | - email 36 | Schema: 37 | - AttributeDataType: String 38 | Name: email 39 | Required: true 40 | - AttributeDataType: String 41 | Name: family_name 42 | Required: true 43 | - AttributeDataType: String 44 | Name: given_name 45 | Required: true 46 | 47 | CognitoUserPoolClient: 48 | Type: AWS::Cognito::UserPoolClient 49 | Properties: 50 | UserPoolId: !Ref CognitoUserPool 51 | ClientName: !Sub ${ProjectName}-${EnvironmentName}-client 52 | GenerateSecret: false 53 | 54 | UserPoolDomain: 55 | Type: AWS::Cognito::UserPoolDomain 56 | Properties: 57 | Domain: !Join ['-', ['geotrack', !Select [1, !Split ['-', !Select [2, !Split ['/', !Ref AWS::StackId]]]]]] 58 | UserPoolId: !Ref CognitoUserPool 59 | 60 | CognitoIdentityPool: 61 | Type: AWS::Cognito::IdentityPool 62 | Properties: 63 | IdentityPoolName: !Sub ${ProjectName}-${EnvironmentName}-identity 64 | AllowUnauthenticatedIdentities: true 65 | CognitoIdentityProviders: 66 | - ClientId: !Ref CognitoUserPoolClient 67 | ProviderName: !GetAtt CognitoUserPool.ProviderName 68 | 69 | # Create a role for unauthorized acces to AWS resources. Very limited access. Only allows users in the previously created Identity Pool 70 | CognitoUnAuthorizedRole: 71 | Type: AWS::IAM::Role 72 | Properties: 73 | AssumeRolePolicyDocument: 74 | Version: "2012-10-17" 75 | Statement: 76 | - Effect: "Allow" 77 | Principal: 78 | Federated: "cognito-identity.amazonaws.com" 79 | Action: 80 | - "sts:AssumeRoleWithWebIdentity" 81 | Condition: 82 | StringEquals: 83 | 'cognito-identity.amazonaws.com:aud': 84 | Ref: CognitoIdentityPool 85 | 'ForAnyValue:StringLike': 86 | 'cognito-identity.amazonaws.com:amr': unauthenticated 87 | Policies: 88 | - PolicyName: MapGeo 89 | PolicyDocument: 90 | Version: "2012-10-17" 91 | Statement: 92 | - Effect: Allow 93 | Action: 94 | - "geo:GetMapStyleDescriptor" 95 | - "geo:GetMapGlyphs" 96 | - "geo:GetMapSprites" 97 | - "geo:GetMapTile" 98 | Resource: !GetAtt Map.Arn 99 | 100 | # Create a role for authorized acces to AWS resources. Control what your user can access. This example only allows Lambda invokation 101 | # Only allows users in the previously created Identity Pool 102 | CognitoAuthorizedRole: 103 | Type: AWS::IAM::Role 104 | Properties: 105 | AssumeRolePolicyDocument: 106 | Version: "2012-10-17" 107 | Statement: 108 | - Effect: "Allow" 109 | Principal: 110 | Federated: "cognito-identity.amazonaws.com" 111 | Action: 112 | - "sts:AssumeRoleWithWebIdentity" 113 | Condition: 114 | StringEquals: 115 | 'cognito-identity.amazonaws.com:aud': 116 | Ref: CognitoIdentityPool 117 | 'ForAnyValue:StringLike': 118 | 'cognito-identity.amazonaws.com:amr': authenticated 119 | Policies: 120 | - PolicyName: Geo 121 | PolicyDocument: 122 | Version: "2012-10-17" 123 | Statement: 124 | - Effect: Allow 125 | Action: 126 | - "geo:GetMapStyleDescriptor" 127 | - "geo:GetMapGlyphs" 128 | - "geo:GetMapSprites" 129 | - "geo:GetMapTile" 130 | Resource: !GetAtt Map.Arn 131 | - Effect: Allow 132 | Action: 133 | - "geo:BatchGetDevicePosition" 134 | - "geo:GetDevicePosition" 135 | - "geo:GetDevicePositionHistory" 136 | Resource: !GetAtt Tracker.Arn 137 | - Effect: Allow 138 | Action: 139 | - "geo:ListGeofences" 140 | - "geo:GetGeofence" 141 | - "geo:PutGeofence" 142 | Resource: !GetAtt GeoFenceCollection.Arn 143 | - Effect: Allow 144 | Action: 145 | - "geo:SearchPlaceIndexForPosition" 146 | - "geo:SearchPlaceIndexForText" 147 | Resource: !GetAtt PlaceIndex.Arn 148 | - Effect: Allow 149 | Action: 150 | - "geo:CalculateRoute" 151 | Resource: !GetAtt RouteCalculation.Arn 152 | 153 | # Assigns the roles to the Identity Pool 154 | IdentityPoolRoleMapping: 155 | Type: AWS::Cognito::IdentityPoolRoleAttachment 156 | Properties: 157 | IdentityPoolId: !Ref CognitoIdentityPool 158 | Roles: 159 | authenticated: !GetAtt CognitoAuthorizedRole.Arn 160 | unauthenticated: !GetAtt CognitoUnAuthorizedRole.Arn 161 | 162 | ApiGw: 163 | Type: AWS::Serverless::Api 164 | Properties: 165 | Name: !Sub ${ProjectName}-api 166 | StageName: !Sub ${EnvironmentName} 167 | Cors: 168 | AllowOrigin: "'*'" 169 | AllowHeaders: "'*'" 170 | AllowMethods: "'OPTIONS,POST,GET,PUT,DELETE'" 171 | Auth: 172 | DefaultAuthorizer: TokenAuthorizer 173 | Authorizers: 174 | TokenAuthorizer: 175 | UserPoolArn: !GetAtt CognitoUserPool.Arn 176 | AddDefaultAuthorizerToCorsPreflight: False 177 | 178 | DriversTable: 179 | Type: AWS::DynamoDB::Table 180 | Properties: 181 | TableName: !Sub ${ProjectName}-${EnvironmentName}-Drivers 182 | BillingMode: PAY_PER_REQUEST 183 | AttributeDefinitions: 184 | - AttributeName: id 185 | AttributeType: S 186 | KeySchema: 187 | - AttributeName: id 188 | KeyType: HASH 189 | TimeToLiveSpecification: 190 | AttributeName: expiration 191 | Enabled: true 192 | 193 | TripsTable: 194 | Type: AWS::DynamoDB::Table 195 | Properties: 196 | TableName: !Sub ${ProjectName}-${EnvironmentName}-Trips 197 | BillingMode: PAY_PER_REQUEST 198 | AttributeDefinitions: 199 | - AttributeName: id 200 | AttributeType: S 201 | - AttributeName: createdAt 202 | AttributeType: S 203 | - AttributeName: status 204 | AttributeType: S 205 | KeySchema: 206 | - AttributeName: id 207 | KeyType: HASH 208 | TimeToLiveSpecification: 209 | AttributeName: expireAt 210 | Enabled: true 211 | GlobalSecondaryIndexes: 212 | - 213 | IndexName: "statusSgi" 214 | KeySchema: 215 | - 216 | AttributeName: status 217 | KeyType: "HASH" 218 | - 219 | AttributeName: createdAt 220 | KeyType: "RANGE" 221 | Projection: 222 | ProjectionType: "ALL" 223 | 224 | GraphQLAPI: 225 | Type: AWS::Serverless::GraphQLApi 226 | Properties: 227 | Name: !Sub ${ProjectName}-${EnvironmentName}-graphql 228 | SchemaUri: ./schema.graphql 229 | Auth: 230 | Type: "AMAZON_COGNITO_USER_POOLS" 231 | UserPool: 232 | UserPoolId: !Ref CognitoUserPool 233 | AwsRegion: !Ref "AWS::Region" 234 | DefaultAction: ALLOW 235 | Additional: 236 | - Type: AWS_IAM 237 | DataSources: 238 | DynamoDb: 239 | DriversTable: 240 | TableName: !Ref DriversTable 241 | TableArn: !GetAtt DriversTable.Arn 242 | TripsTable: 243 | TableName: !Ref TripsTable 244 | TableArn: !GetAtt TripsTable.Arn 245 | Functions: 246 | listDrivers: 247 | Runtime: 248 | Name: APPSYNC_JS 249 | Version: "1.0.0" 250 | DataSource: DriversTable 251 | CodeUri: ./webapp/src/resolvers/listDrivers.js 252 | listTrips: 253 | Runtime: 254 | Name: APPSYNC_JS 255 | Version: "1.0.0" 256 | DataSource: TripsTable 257 | CodeUri: ./webapp/src/resolvers/listTrips.js 258 | statusTrips: 259 | Runtime: 260 | Name: APPSYNC_JS 261 | Version: "1.0.0" 262 | DataSource: TripsTable 263 | CodeUri: ./webapp/src/resolvers/statusTrips.js 264 | hydrateTrips: 265 | Runtime: 266 | Name: APPSYNC_JS 267 | Version: "1.0.0" 268 | DataSource: DriversTable 269 | CodeUri: ./webapp/src/resolvers/hydrateTrips.js 270 | getTrip: 271 | Runtime: 272 | Name: APPSYNC_JS 273 | Version: "1.0.0" 274 | DataSource: TripsTable 275 | CodeUri: ./webapp/src/resolvers/getById.js 276 | getDriver: 277 | Runtime: 278 | Name: APPSYNC_JS 279 | Version: "1.0.0" 280 | DataSource: DriversTable 281 | CodeUri: ./webapp/src/resolvers/getById.js 282 | delDriver: 283 | Runtime: 284 | Name: APPSYNC_JS 285 | Version: "1.0.0" 286 | DataSource: DriversTable 287 | CodeUri: ./webapp/src/resolvers/delById.js 288 | saveDriver: 289 | Runtime: 290 | Name: APPSYNC_JS 291 | Version: "1.0.0" 292 | DataSource: DriversTable 293 | CodeUri: ./webapp/src/resolvers/saveById.js 294 | addDriverTrip: 295 | Runtime: 296 | Name: APPSYNC_JS 297 | Version: "1.0.0" 298 | DataSource: DriversTable 299 | CodeUri: ./webapp/src/resolvers/addDriverTrip.js 300 | removeDriverTrip: 301 | Runtime: 302 | Name: APPSYNC_JS 303 | Version: "1.0.0" 304 | DataSource: DriversTable 305 | CodeUri: ./webapp/src/resolvers/removeDriverTrip.js 306 | saveTrip: 307 | Runtime: 308 | Name: APPSYNC_JS 309 | Version: "1.0.0" 310 | DataSource: TripsTable 311 | CodeUri: ./webapp/src/resolvers/saveById.js 312 | delTrip: 313 | Runtime: 314 | Name: APPSYNC_JS 315 | Version: "1.0.0" 316 | DataSource: TripsTable 317 | CodeUri: ./webapp/src/resolvers/delById.js 318 | Resolvers: 319 | Mutation: 320 | saveDriver: 321 | Runtime: 322 | Name: APPSYNC_JS 323 | Version: "1.0.0" 324 | Pipeline: 325 | - saveDriver 326 | delDriver: 327 | Runtime: 328 | Name: APPSYNC_JS 329 | Version: "1.0.0" 330 | Pipeline: 331 | - delDriver 332 | saveTrip: 333 | Runtime: 334 | Name: APPSYNC_JS 335 | Version: "1.0.0" 336 | Pipeline: 337 | - saveTrip 338 | - addDriverTrip 339 | delTrip: 340 | Runtime: 341 | Name: APPSYNC_JS 342 | Version: "1.0.0" 343 | Pipeline: 344 | - delTrip 345 | - removeDriverTrip 346 | Query: 347 | listTrips: 348 | Runtime: 349 | Name: APPSYNC_JS 350 | Version: "1.0.0" 351 | Pipeline: 352 | - listTrips 353 | - hydrateTrips 354 | statusTrips: 355 | Runtime: 356 | Name: APPSYNC_JS 357 | Version: "1.0.0" 358 | Pipeline: 359 | - statusTrips 360 | - hydrateTrips 361 | listDrivers: 362 | Runtime: 363 | Name: APPSYNC_JS 364 | Version: "1.0.0" 365 | Pipeline: 366 | - listDrivers 367 | 368 | S3WebAppBucket: 369 | Type: AWS::S3::Bucket 370 | Properties: 371 | AccessControl: Private 372 | BucketEncryption: 373 | ServerSideEncryptionConfiguration: 374 | - ServerSideEncryptionByDefault: 375 | SSEAlgorithm: AES256 376 | DeletionPolicy: Delete 377 | 378 | S3WebAppBucketPolicy: 379 | Type: AWS::S3::BucketPolicy 380 | Properties: 381 | Bucket: !Ref S3WebAppBucket 382 | PolicyDocument: 383 | Statement: 384 | - Action: s3:GetObject 385 | Effect: Allow 386 | Resource: !Sub ${S3WebAppBucket.Arn}/* 387 | Principal: 388 | Service: cloudfront.amazonaws.com 389 | Condition: 390 | StringEquals: 391 | AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFront} 392 | 393 | OriginAccessControl: 394 | Type: AWS::CloudFront::OriginAccessControl 395 | Properties: 396 | OriginAccessControlConfig: 397 | Name: !Sub ${ProjectName}-${EnvironmentName}-OAC 398 | Description: Default Origin Access Control 399 | OriginAccessControlOriginType: s3 400 | SigningBehavior: always 401 | SigningProtocol: sigv4 402 | 403 | CloudFront: 404 | Type: AWS::CloudFront::Distribution 405 | Properties: 406 | DistributionConfig: 407 | Origins: 408 | - Id: S3Origin 409 | DomainName: !GetAtt S3WebAppBucket.DomainName 410 | S3OriginConfig: 411 | OriginAccessIdentity: '' 412 | OriginAccessControlId: !GetAtt OriginAccessControl.Id 413 | Enabled: true 414 | DefaultRootObject: index.html 415 | DefaultCacheBehavior: 416 | ViewerProtocolPolicy: redirect-to-https 417 | AllowedMethods: 418 | - GET 419 | - HEAD 420 | - OPTIONS 421 | CachedMethods: 422 | - GET 423 | - HEAD 424 | - OPTIONS 425 | TargetOriginId: S3Origin 426 | ForwardedValues: 427 | QueryString: false 428 | Cookies: 429 | Forward: none 430 | 431 | CoreLayer: 432 | Type: AWS::Serverless::LayerVersion 433 | Properties: 434 | Description: requests 435 | ContentUri: ./layers/requests 436 | CompatibleRuntimes: 437 | - python3.10 438 | RetentionPolicy: Delete 439 | Metadata: 440 | BuildMethod: python3.10 441 | 442 | LaunchDeliveryFleet: 443 | Type: AWS::Serverless::Function 444 | Properties: 445 | FunctionName: !Sub "${ProjectName}-${EnvironmentName}-LaunchDeliveryFleet" 446 | CodeUri: ./lambdas/simulation/launchDeliveryFleet 447 | Layers: 448 | - !Ref CoreLayer 449 | Events: 450 | Api: 451 | Type: Api 452 | Properties: 453 | RestApiId: !Ref ApiGw 454 | Path: /launch 455 | Method: POST 456 | Policies: 457 | - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess 458 | - SSMParameterReadPolicy: 459 | ParameterName: !Sub "${ProjectName}/*" 460 | - LambdaInvokePolicy: 461 | FunctionName: !Ref PushVehiclePosition 462 | - Version: '2012-10-17' # Policy Document 463 | Statement: 464 | - Effect: Allow 465 | Action: 466 | - "appsync:GraphQL" 467 | - "appsync:GetGraphqlApi" 468 | - "appsync:ListGraphqlApis" 469 | - "appsync:ListApiKeys" 470 | Resource: '*' 471 | Environment: 472 | Variables: 473 | PUSH_VEHICLE_LAMBDA_NAME: !Ref PushVehiclePosition 474 | PROJECT_NAME: !Ref ProjectName 475 | PROJECT_ENV: !Ref EnvironmentName 476 | APPSYNC_URL: !GetAtt GraphQLAPI.GraphQLUrl 477 | 478 | PushVehiclePosition: 479 | Type: AWS::Serverless::Function 480 | Properties: 481 | FunctionName: !Sub "${ProjectName}-${EnvironmentName}-PushVehiclePosition" 482 | CodeUri: ./lambdas/simulation/pushVehiclePosition 483 | Timeout: 600 484 | Policies: 485 | - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess 486 | - SSMParameterReadPolicy: 487 | ParameterName: !Sub "${ProjectName}/*" 488 | - Version: '2012-10-17' # Policy Document 489 | Statement: 490 | - Effect: Allow 491 | Action: 492 | - "iot:Connect" 493 | - "iot:Publish" 494 | - "iot:Subscribe" 495 | - "iot:Receive" 496 | - "iot:GetThingShadow" 497 | - "iot:UpdateThingShadow" 498 | - "iot:DeleteThingShadow" 499 | - "iot:ListNamedShadowsForThing" 500 | - "geo:CalculateRoute" 501 | - "geo:ListRouteCalculators" 502 | Resource: '*' 503 | Environment: 504 | Variables: 505 | IOT_TOPIC: !Sub "${ProjectName}/positions" 506 | PROJECT_NAME: !Ref ProjectName 507 | PROJECT_ENV: !Ref EnvironmentName 508 | ROUTE_NAME: !Ref RouteCalculation 509 | TRACKER_NAME: !Ref Tracker 510 | 511 | # IoTUpdateTracker: 512 | # Type: AWS::Serverless::Function 513 | # Properties: 514 | # FunctionName: !Sub "${ProjectName}-${EnvironmentName}-IoTUpdateTracker" 515 | # CodeUri: ./lambdas/iot 516 | # Events: 517 | # EventBusRule: 518 | # Type: IoTRule 519 | # Properties: 520 | # Sql: !Sub "SELECT * FROM '${ProjectName}/positions'" 521 | # Policies: 522 | # - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess 523 | # - SSMParameterReadPolicy: 524 | # ParameterName: !Sub "${ProjectName}/*" 525 | # - Version: '2012-10-17' # Policy Document 526 | # Statement: 527 | # - Effect: Allow 528 | # Action: 529 | # - "geo:ListTrackers" 530 | # - "geo:ListTrackerConsumers" 531 | # - "geo:BatchUpdateDevicePosition" 532 | # - "geo:BatchGetDevicePosition" 533 | # - "geo:GetDevicePositionHistory" 534 | # - "geo:DescribeTracker" 535 | # - "geo:UpdateTracker" 536 | # Resource: "*" 537 | # Environment: 538 | # Variables: 539 | # PROJECT_NAME: !Ref ProjectName 540 | # PROJECT_ENV: !Ref EnvironmentName 541 | # TRAKER: !Ref Tracker 542 | 543 | IoTTopicRuleRole: 544 | Type: AWS::IAM::Role 545 | Properties: 546 | AssumeRolePolicyDocument: 547 | Version: "2012-10-17" 548 | Statement: 549 | - Effect: Allow 550 | Principal: 551 | Service: 552 | - iot.amazonaws.com 553 | Action: 554 | - 'sts:AssumeRole' 555 | Policies: 556 | - PolicyName: location 557 | PolicyDocument: 558 | Statement: 559 | - Effect: Allow 560 | Action: 561 | - "geo:ListTrackers" 562 | - "geo:ListTrackerConsumers" 563 | - "geo:BatchUpdateDevicePosition" 564 | - "geo:BatchGetDevicePosition" 565 | - "geo:GetDevicePositionHistory" 566 | - "geo:DescribeTracker" 567 | - "geo:UpdateTracker" 568 | Resource: "*" 569 | 570 | IoTTopicRule: 571 | Type: AWS::IoT::TopicRule 572 | Properties: 573 | RuleName: !Sub "${ProjectName}_${EnvironmentName}_IoTRuleLocation" 574 | TopicRulePayload: 575 | AwsIotSqlVersion: "2016-03-23" 576 | RuleDisabled: false 577 | Sql: !Sub "SELECT * FROM '${ProjectName}/positions'" 578 | Actions: 579 | - Location: 580 | DeviceId: "${DeviceID}" 581 | Latitude: "${get(position, 0)}" 582 | Longitude: "${get(position, 1)}" 583 | RoleArn: !GetAtt IoTTopicRuleRole.Arn 584 | Timestamp: 585 | Value: "${timestamp()}" 586 | Unit: "MILLISECONDS" 587 | TrackerName: !Ref Tracker 588 | 589 | PinpointProject: 590 | Type: AWS::Pinpoint::App 591 | Properties: 592 | Name: !Sub "${ProjectName}-${EnvironmentName}-PinPoint" 593 | 594 | PinpointSMSChannel: 595 | Type: AWS::Pinpoint::SMSChannel 596 | Properties: 597 | ApplicationId: !Ref PinpointProject 598 | Enabled: true 599 | 600 | EventBus: 601 | Type: AWS::Events::EventBus 602 | Properties: 603 | Name: !Sub "${ProjectName}-${EnvironmentName}-EventBus" 604 | 605 | EventBridgeResponse: 606 | Type: AWS::Serverless::Function 607 | Properties: 608 | FunctionName: !Sub "${ProjectName}-${EnvironmentName}-EventBridgeResponse" 609 | CodeUri: ./lambdas/eventbridge 610 | Layers: 611 | - !Ref CoreLayer 612 | Policies: 613 | - arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess 614 | - PinpointEndpointAccessPolicy: 615 | PinpointApplicationId: !Ref PinpointProject 616 | - SSMParameterReadPolicy: 617 | ParameterName: !Sub "${ProjectName}/*" 618 | - Version: '2012-10-17' # Policy Document 619 | Statement: 620 | - Effect: Allow 621 | Action: 622 | - "appsync:GraphQL" 623 | - "appsync:GetGraphqlApi" 624 | - "appsync:ListGraphqlApis" 625 | - "appsync:ListApiKeys" 626 | Resource: '*' 627 | Environment: 628 | Variables: 629 | APPLICATION_ID: !Ref PinpointProject 630 | PROJECT_NAME: !Ref ProjectName 631 | PROJECT_ENV: !Ref EnvironmentName 632 | APPSYNC_URL: !GetAtt GraphQLAPI.GraphQLUrl 633 | 634 | 635 | GeoFenceCollection: 636 | Type: AWS::Location::GeofenceCollection 637 | Properties: 638 | CollectionName: !Sub ${ProjectName}-${EnvironmentName}-GeoFence 639 | 640 | RouteCalculation: 641 | Type: AWS::Location::RouteCalculator 642 | Properties: 643 | CalculatorName: !Sub ${ProjectName}-${EnvironmentName}-RouteCalculator 644 | DataSource: Esri 645 | PricingPlan: RequestBasedUsage 646 | 647 | Map: 648 | Type: AWS::Location::Map 649 | Properties: 650 | Configuration: 651 | Style: VectorEsriStreets 652 | MapName: !Sub ${ProjectName}-${EnvironmentName}-Map 653 | PricingPlan: RequestBasedUsage 654 | 655 | PlaceIndex: 656 | Type: AWS::Location::PlaceIndex 657 | Properties: 658 | DataSource: Esri 659 | DataSourceConfiguration: 660 | IntendedUse: "SingleUse" 661 | IndexName: !Sub ${ProjectName}-${EnvironmentName}-PlaceIndex 662 | PricingPlan: RequestBasedUsage 663 | 664 | Tracker: 665 | Type: AWS::Location::Tracker 666 | Properties: 667 | PositionFiltering: TimeBased 668 | TrackerName: !Sub ${ProjectName}-${EnvironmentName}-Tracker 669 | 670 | TrackerConsumer: 671 | Type: AWS::Location::TrackerConsumer 672 | DependsOn: GeoFenceCollection 673 | Properties: 674 | ConsumerArn: !GetAtt GeoFenceCollection.Arn 675 | TrackerName: !Ref Tracker 676 | 677 | GeoRule: 678 | Type: AWS::Events::Rule 679 | Properties: 680 | Name: !Sub "${ProjectName}-${EnvironmentName}-EventRule" 681 | EventBusName: !Ref EventBus 682 | State: ENABLED 683 | EventPattern: 684 | source: 685 | - "aws.geo" 686 | detail-type: 687 | - "Location Geofence Event" 688 | detail: 689 | EventType: 690 | - "ENTER" 691 | Targets: 692 | - 693 | Arn: !GetAtt EventBridgeResponse.Arn 694 | Id: "geoTrackV1" 695 | 696 | PermissionForEventsToInvokeLambda: 697 | Type: AWS::Lambda::Permission 698 | Properties: 699 | FunctionName: !Ref "EventBridgeResponse" 700 | Action: "lambda:InvokeFunction" 701 | Principal: "events.amazonaws.com" 702 | SourceArn: !GetAtt GeoRule.Arn 703 | 704 | 705 | Outputs: 706 | S3WebAppBucket: 707 | Description: S3 Web App Name 708 | Value: !Ref S3WebAppBucket 709 | 710 | IdentityPoolId: 711 | Description: Cognito IdentityPool Id 712 | Value: !Ref CognitoIdentityPool 713 | 714 | CognitoUserPoolClientId: 715 | Description: Cognito UserPool ClientId 716 | Value: !Ref CognitoUserPoolClient 717 | 718 | CognitoUserPoolId: 719 | Description: Cognito UserPool Id 720 | Value: !Ref CognitoUserPool 721 | 722 | CognitoDomainName: 723 | Description: Cognito DomainName 724 | Value: !Ref UserPoolDomain 725 | 726 | ApiId: 727 | Description: API Id 728 | Value: !Ref ApiGw 729 | 730 | CloudFrontUrl: 731 | Description: CloudFront Url 732 | Value: !GetAtt CloudFront.DomainName 733 | 734 | AppSyncUrl: 735 | Description: AppSync Url 736 | Value: !GetAtt GraphQLAPI.GraphQLUrl 737 | 738 | AwsRegion: 739 | Description: AWS Region Deployed 740 | Value: !Sub ${AWS::Region} 741 | 742 | GeoMap: 743 | Description: Amazon Location Map 744 | Value: !Ref Map 745 | 746 | GeoRouteCalculation: 747 | Description: Amazon Location Route 748 | Value: !Ref RouteCalculation 749 | 750 | GeoPlaceIndex: 751 | Description: Amazon Location PlaceIndex 752 | Value: !Ref PlaceIndex 753 | 754 | GeoTracker: 755 | Description: Amazon Location Tracker 756 | Value: !Ref Tracker 757 | 758 | GeoFence: 759 | Description: Amazon Location GeoFenceCollection 760 | Value: !Ref GeoFenceCollection 761 | 762 | LaunchDeliveryFleet: 763 | Value: !GetAtt LaunchDeliveryFleet.Arn 764 | 765 | PushVehiclePosition: 766 | Value: !GetAtt PushVehiclePosition.Arn 767 | -------------------------------------------------------------------------------- /webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | 30 | # Code 31 | *.env 32 | samconfig.toml 33 | .aws-sam 34 | -------------------------------------------------------------------------------- /webapp/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Vite App 10 | 11 | 12 |
13 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /webapp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webapp", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "preview": "vite preview" 9 | }, 10 | "dependencies": { 11 | "@aws-amplify/ui-vue": "^4.0.0", 12 | "@aws-sdk/client-location": "^3.451.0", 13 | "@aws-sdk/signature-v4": "^3.374.0", 14 | "@aws/amazon-location-utilities-auth-helper": "^1.0.3", 15 | "@turf/circle": "^6.5.0", 16 | "aws-amplify": "^6.0.2", 17 | "axios": "^1.6.2", 18 | "maplibre-gl": "^3.6.1", 19 | "material-design-icons": "^3.0.1", 20 | "pinia": "^2.1.7", 21 | "vite-plugin-svgr": "^4.1.0", 22 | "vite-tsconfig-paths": "^4.2.1", 23 | "vue": "^3.3.4", 24 | "vue-router": "^4.2.5", 25 | "vuetify": "^3.4.2" 26 | }, 27 | "devDependencies": { 28 | "@vitejs/plugin-vue": "^4.5.0", 29 | "vite": "^4.5.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-location-service-geotrack-vuejs/400380be7c834b58de69c70600a7fdd83d0303f5/webapp/public/favicon.ico -------------------------------------------------------------------------------- /webapp/src/App.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 43 | 44 | 45 | // https://www.vuemastery.com/blog/exploring-vuetify-3/ -------------------------------------------------------------------------------- /webapp/src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | font-weight: normal; 59 | } 60 | 61 | body { 62 | min-height: 100vh; 63 | color: var(--color-text); 64 | background: var(--color-background); 65 | transition: color 0.5s, background-color 0.5s; 66 | line-height: 1.6; 67 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 68 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 69 | font-size: 15px; 70 | text-rendering: optimizeLegibility; 71 | -webkit-font-smoothing: antialiased; 72 | -moz-osx-font-smoothing: grayscale; 73 | } 74 | -------------------------------------------------------------------------------- /webapp/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /webapp/src/assets/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | #app { 4 | max-width: 1280px; 5 | margin: 0 auto; 6 | padding: 2rem; 7 | 8 | font-weight: normal; 9 | } 10 | 11 | a, 12 | .green { 13 | text-decoration: none; 14 | color: hsla(160, 100%, 37%, 1); 15 | transition: 0.4s; 16 | } 17 | 18 | @media (hover: hover) { 19 | a:hover { 20 | background-color: hsla(160, 100%, 37%, 0.2); 21 | } 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | body { 26 | display: flex; 27 | place-items: center; 28 | } 29 | 30 | #app { 31 | display: grid; 32 | grid-template-columns: 1fr 1fr; 33 | padding: 0 2rem; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /webapp/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 67 | 68 | 154 | -------------------------------------------------------------------------------- /webapp/src/components/Map.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 403 | 404 | -------------------------------------------------------------------------------- /webapp/src/configAmplify.js: -------------------------------------------------------------------------------- 1 | import { Amplify } from 'aws-amplify'; 2 | 3 | export function configAmplify() { 4 | 5 | Amplify.configure({ 6 | Auth: { 7 | Cognito: { 8 | identityPoolId: import.meta.env.VITE_IDENTITY_POOL_ID, // REQUIRED - Amazon Cognito Identity Pool ID 9 | region: import.meta.env.VITE_AWS_REGION, // REQUIRED - Amazon Cognito Region 10 | userPoolId: import.meta.env.VITE_COGNITO_USER_POOL_ID, // OPTIONAL - Amazon Cognito User Pool ID for authenticated user access 11 | userPoolClientId: import.meta.env.VITE_COGNITO_USER_POOL_CLIENT_ID, // OPTIONAL - Amazon Cognito Web Client ID for authenticated user access 12 | }, 13 | }, 14 | API: { 15 | GraphQL: { 16 | endpoint: import.meta.env.VITE_GRAPHQL_ENDPOINT, 17 | region: import.meta.env.VITE_AWS_REGION, 18 | defaultAuthMode: 'userPool' 19 | } 20 | }, 21 | Geo: { 22 | LocationService: { 23 | maps: { 24 | items: { 25 | [import.meta.env.VITE_GEOMAP]: { // REQUIRED - Amazon Location Service Map resource name 26 | style: "VectorEsriStreets", // REQUIRED - String representing the style of map resource 27 | }, 28 | }, 29 | default: import.meta.env.VITE_GEOMAP, // REQUIRED - Amazon Location Service Map resource name to set as default 30 | }, 31 | search_indices: { 32 | items: [import.meta.env.VITE_GEOPLACE_INDEX], // REQUIRED - Amazon Location Service Place Index name 33 | default: import.meta.env.VITE_GEOPLACE_INDEX, // REQUIRED - Amazon Location Service Place Index name to set as default 34 | }, 35 | region: import.meta.env.VITE_AWS_REGION // REQUIRED - Amazon Location Service Region 36 | }, 37 | }, 38 | }) 39 | 40 | } -------------------------------------------------------------------------------- /webapp/src/graphql/mutations.js: -------------------------------------------------------------------------------- 1 | 2 | export const saveDriver = /* GraphQL */ ` 3 | mutation SaveDriver ( 4 | $input: DriverInput! 5 | ) { 6 | saveDriver(input: $input) { 7 | id 8 | fullName 9 | email 10 | createdAt 11 | trips { 12 | id 13 | createdAt 14 | } 15 | } 16 | } 17 | `; 18 | 19 | export const delDriver = /* GraphQL */ ` 20 | mutation DeleteDriver ( 21 | $id: ID! 22 | ) { 23 | delDriver(id: $id) { 24 | id 25 | } 26 | } 27 | `; 28 | 29 | export const saveTrip = /* GraphQL */ ` 30 | mutation SaveTrip ( 31 | $input: TripInput! 32 | ) { 33 | saveTrip(input: $input) { 34 | id 35 | driver { 36 | id 37 | } 38 | createdAt 39 | } 40 | } 41 | `; 42 | 43 | export const delTrip = /* GraphQL */ ` 44 | mutation DeleteTrip ( 45 | $id: ID! 46 | ) { 47 | delTrip(id: $id) { 48 | id 49 | } 50 | } 51 | `; -------------------------------------------------------------------------------- /webapp/src/graphql/queries.js: -------------------------------------------------------------------------------- 1 | export const listDrivers = /* GraphQL */ ` 2 | query ListDrivers { 3 | listDrivers { 4 | drivers { 5 | deliveryType 6 | deviceId 7 | deviceType 8 | email 9 | fullName 10 | status 11 | id 12 | } 13 | } 14 | } 15 | `; 16 | 17 | export const deviceIdByTripStatus = /* GraphQL */ ` 18 | query DeviceIdByTripStatus ( 19 | $status: TripStatus 20 | ) { 21 | statusTrips(status: $status) { 22 | nextToken 23 | trips { 24 | driver { 25 | deviceId 26 | } 27 | } 28 | } 29 | } 30 | `; 31 | 32 | export const listTrips = /* GraphQL */ ` 33 | query ListTrips { 34 | listTrips { 35 | trips { 36 | createdAt 37 | duration 38 | distance 39 | labelStart 40 | geoStart { 41 | lat 42 | lng 43 | } 44 | labelEnd 45 | geoEnd { 46 | lat 47 | lng 48 | } 49 | driver { 50 | id 51 | fullName 52 | } 53 | duration 54 | id 55 | status 56 | } 57 | } 58 | } 59 | `; -------------------------------------------------------------------------------- /webapp/src/graphql/subscriptions.js: -------------------------------------------------------------------------------- 1 | 2 | export const onCreateDeliveryAgent = /* GraphQL */ ` 3 | subscription OnCreateDeliveryAgent($owner: String) { 4 | onCreateDeliveryAgent(owner: $owner) { 5 | id 6 | fullName 7 | deliveryType 8 | device { 9 | id 10 | deliveryAgentId 11 | deviceType 12 | createdAt 13 | updatedAt 14 | owner 15 | } 16 | createdAt 17 | updatedAt 18 | deliveryAgentDeviceId 19 | owner 20 | } 21 | } 22 | `; 23 | export const onUpdateDeliveryAgent = /* GraphQL */ ` 24 | subscription OnUpdateDeliveryAgent($owner: String) { 25 | onUpdateDeliveryAgent(owner: $owner) { 26 | id 27 | fullName 28 | deliveryType 29 | device { 30 | id 31 | deliveryAgentId 32 | deviceType 33 | createdAt 34 | updatedAt 35 | owner 36 | } 37 | createdAt 38 | updatedAt 39 | deliveryAgentDeviceId 40 | owner 41 | } 42 | } 43 | `; 44 | export const onDeleteDeliveryAgent = /* GraphQL */ ` 45 | subscription OnDeleteDeliveryAgent($owner: String) { 46 | onDeleteDeliveryAgent(owner: $owner) { 47 | id 48 | fullName 49 | deliveryType 50 | device { 51 | id 52 | deliveryAgentId 53 | deviceType 54 | createdAt 55 | updatedAt 56 | owner 57 | } 58 | createdAt 59 | updatedAt 60 | deliveryAgentDeviceId 61 | owner 62 | } 63 | } 64 | `; 65 | export const onCreateDevice = /* GraphQL */ ` 66 | subscription OnCreateDevice($owner: String) { 67 | onCreateDevice(owner: $owner) { 68 | id 69 | deliveryAgentId 70 | deviceType 71 | createdAt 72 | updatedAt 73 | owner 74 | } 75 | } 76 | `; 77 | export const onUpdateDevice = /* GraphQL */ ` 78 | subscription OnUpdateDevice($owner: String) { 79 | onUpdateDevice(owner: $owner) { 80 | id 81 | deliveryAgentId 82 | deviceType 83 | createdAt 84 | updatedAt 85 | owner 86 | } 87 | } 88 | `; 89 | export const onDeleteDevice = /* GraphQL */ ` 90 | subscription OnDeleteDevice($owner: String) { 91 | onDeleteDevice(owner: $owner) { 92 | id 93 | deliveryAgentId 94 | deviceType 95 | createdAt 96 | updatedAt 97 | owner 98 | } 99 | } 100 | `; 101 | export const onCreateDeliveryInfo = /* GraphQL */ ` 102 | subscription OnCreateDeliveryInfo($owner: String) { 103 | onCreateDeliveryInfo(owner: $owner) { 104 | id 105 | deliveryAgent { 106 | id 107 | fullName 108 | deliveryType 109 | device { 110 | id 111 | deliveryAgentId 112 | deviceType 113 | createdAt 114 | updatedAt 115 | owner 116 | } 117 | createdAt 118 | updatedAt 119 | deliveryAgentDeviceId 120 | owner 121 | } 122 | geoStart { 123 | lat 124 | lng 125 | } 126 | geoEnd { 127 | lat 128 | lng 129 | } 130 | duration 131 | distance 132 | geoFenceId 133 | userPhone 134 | expireAt 135 | status 136 | createdAt 137 | updatedAt 138 | deliveryInfoDeliveryAgentId 139 | owner 140 | } 141 | } 142 | `; 143 | export const onUpdateDeliveryInfo = /* GraphQL */ ` 144 | subscription OnUpdateDeliveryInfo($owner: String) { 145 | onUpdateDeliveryInfo(owner: $owner) { 146 | id 147 | deliveryAgent { 148 | id 149 | fullName 150 | deliveryType 151 | device { 152 | id 153 | deliveryAgentId 154 | deviceType 155 | createdAt 156 | updatedAt 157 | owner 158 | } 159 | createdAt 160 | updatedAt 161 | deliveryAgentDeviceId 162 | owner 163 | } 164 | geoStart { 165 | lat 166 | lng 167 | } 168 | geoEnd { 169 | lat 170 | lng 171 | } 172 | duration 173 | distance 174 | geoFenceId 175 | userPhone 176 | expireAt 177 | status 178 | createdAt 179 | updatedAt 180 | deliveryInfoDeliveryAgentId 181 | owner 182 | } 183 | } 184 | `; 185 | export const onDeleteDeliveryInfo = /* GraphQL */ ` 186 | subscription OnDeleteDeliveryInfo($owner: String) { 187 | onDeleteDeliveryInfo(owner: $owner) { 188 | id 189 | deliveryAgent { 190 | id 191 | fullName 192 | deliveryType 193 | device { 194 | id 195 | deliveryAgentId 196 | deviceType 197 | createdAt 198 | updatedAt 199 | owner 200 | } 201 | createdAt 202 | updatedAt 203 | deliveryAgentDeviceId 204 | owner 205 | } 206 | geoStart { 207 | lat 208 | lng 209 | } 210 | geoEnd { 211 | lat 212 | lng 213 | } 214 | duration 215 | distance 216 | geoFenceId 217 | userPhone 218 | expireAt 219 | status 220 | createdAt 221 | updatedAt 222 | deliveryInfoDeliveryAgentId 223 | owner 224 | } 225 | } 226 | `; -------------------------------------------------------------------------------- /webapp/src/layouts/SimpleLayout.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /webapp/src/main.js: -------------------------------------------------------------------------------- 1 | //import './assets/main.css' 2 | 3 | import { createApp } from 'vue' 4 | import { createPinia } from 'pinia' 5 | 6 | import App from './App.vue' 7 | import router from './router' 8 | 9 | // Vuetify 10 | import 'vuetify/styles' 11 | import { createVuetify } from 'vuetify' 12 | import * as components from 'vuetify/components' 13 | import * as directives from 'vuetify/directives' 14 | 15 | const vuetify = createVuetify({ 16 | components, 17 | directives, 18 | }) 19 | 20 | const app = createApp(App) 21 | 22 | app.use(createPinia()) 23 | app.use(vuetify) 24 | app.use(router) 25 | 26 | app.mount('#app') 27 | -------------------------------------------------------------------------------- /webapp/src/resolvers/addDriverTrip.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils' 2 | 3 | export function request(ctx) { 4 | 5 | const tripId = ctx.prev.result.id 6 | const driverId = ctx.prev.result.driver.id 7 | const timeStamp = util.time.nowISO8601() 8 | 9 | const expressionValues = util.dynamodb.toMapValues({ ':updatedAt': timeStamp }) 10 | expressionValues[':trips'] = util.dynamodb.toStringSet([tripId]) 11 | 12 | return { 13 | operation: 'UpdateItem', 14 | key: util.dynamodb.toMapValues({ "id": driverId }), 15 | update: { 16 | //expression: "SET trips = list_append(if_not_exists(trips, :emptyList), :trips), updatedAt = :updatedAt", 17 | expression: "ADD trips :trips SET updatedAt = :updatedAt", 18 | expressionValues, 19 | }, 20 | } 21 | } 22 | 23 | export const response = (ctx) => ctx.result 24 | 25 | // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.UpdateExpressions.html#Expressions.UpdateExpressions.SET -------------------------------------------------------------------------------- /webapp/src/resolvers/delById.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils' 2 | import * as ddb from '@aws-appsync/utils/dynamodb'; 3 | 4 | export function request(ctx) { 5 | let condition = null; 6 | if (ctx.args.expectedVersion) { 7 | condition = { id: { attributeExists: false } }; 8 | } 9 | return ddb.remove({ key: { id: ctx.args.id }, condition }); 10 | } 11 | 12 | export function response(ctx) { 13 | const { error, result } = ctx; 14 | if (error) { 15 | util.appendError(error.message, error.type); 16 | } 17 | return result; 18 | } -------------------------------------------------------------------------------- /webapp/src/resolvers/getById.js: -------------------------------------------------------------------------------- 1 | import * as ddb from '@aws-appsync/utils/dynamodb' 2 | 3 | export function request(ctx) { 4 | return ddb.get({ key: { id: ctx.args.id } }) 5 | } 6 | 7 | export const response = (ctx) => ctx.result -------------------------------------------------------------------------------- /webapp/src/resolvers/hydrateTrips.js: -------------------------------------------------------------------------------- 1 | import { util, runtime } from '@aws-appsync/utils' 2 | 3 | export function request(ctx) { 4 | 5 | if (ctx.prev.result.trips.length == 0) { 6 | runtime.earlyReturn({ trips: []}) 7 | } 8 | 9 | let drivers = [] 10 | let ids = [] 11 | 12 | for (const idx in ctx.prev.result.trips) { 13 | let record = ctx.prev.result.trips[idx] 14 | if (record.driver.id != null) { 15 | if (!ids.includes(record.driver.id)) { 16 | ids.push(record.driver.id) 17 | drivers.push(util.dynamodb.toMapValues({ "id": record.driver.id })) 18 | } 19 | } 20 | } 21 | 22 | return dynamoDBBatchGetItem(drivers) 23 | } 24 | 25 | export function response(ctx) { 26 | if (ctx.error) { 27 | util.error(ctx.error.message, ctx.error.type); 28 | } 29 | 30 | let trips = ctx.prev.result.trips 31 | let drivers = ctx.result.data["geotrack-dev-Drivers"] 32 | for (const idxTrips in trips) { 33 | let record = trips[idxTrips] 34 | for (const idxDrivers in drivers) { 35 | let driver = drivers[idxDrivers] 36 | if (record.driver.id == driver.id) { 37 | record.driver = driver 38 | break; 39 | } 40 | } 41 | } 42 | return { trips }; 43 | } 44 | 45 | function dynamoDBBatchGetItem(drivers) { 46 | return { 47 | operation: 'BatchGetItem', 48 | tables: { 49 | "geotrack-dev-Drivers": { 50 | keys: drivers 51 | } 52 | }, 53 | }; 54 | } -------------------------------------------------------------------------------- /webapp/src/resolvers/listDrivers.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils' 2 | 3 | export function request(ctx) { 4 | const { limit = 20, nextToken } = ctx.arguments; 5 | return { operation: 'Scan', limit, nextToken }; 6 | // const { title } = ctx.args; 7 | // const filter = { filter: { beginsWith: title } }; 8 | // return { 9 | // operation: 'Scan', 10 | // filter: JSON.parse(util.transform.toDynamoDBFilterExpression(filter)), 11 | // }; 12 | } 13 | export function response(ctx) { 14 | const { items: drivers = [], nextToken } = ctx.result; 15 | return { drivers, nextToken }; 16 | } -------------------------------------------------------------------------------- /webapp/src/resolvers/listTrips.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | export function request(ctx) { 4 | const { limit = 20, nextToken } = ctx.arguments; 5 | return { operation: 'Scan', limit, nextToken }; 6 | // const { title } = ctx.args; 7 | // const filter = { filter: { beginsWith: title } }; 8 | // return { 9 | // operation: 'Scan', 10 | // filter: JSON.parse(util.transform.toDynamoDBFilterExpression(filter)), 11 | // }; 12 | } 13 | 14 | export function response(ctx) { 15 | const { items: trips = [], nextToken } = ctx.result; 16 | return { trips, nextToken }; 17 | } -------------------------------------------------------------------------------- /webapp/src/resolvers/removeDriverTrip.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils' 2 | 3 | export function request(ctx) { 4 | 5 | const tripId = ctx.prev.result.id 6 | const driverId = ctx.prev.result.driver.id 7 | const timeStamp = util.time.nowISO8601() 8 | 9 | const expressionValues = util.dynamodb.toMapValues({ ':updatedAt': timeStamp }) 10 | expressionValues[':trips'] = util.dynamodb.toStringSet([tripId]) 11 | 12 | return { 13 | operation: 'UpdateItem', 14 | key: util.dynamodb.toMapValues({ "id": driverId }), 15 | update: { 16 | expression: "DELETE trips :trips SET updatedAt = :updatedAt", 17 | expressionValues, 18 | }, 19 | } 20 | } 21 | 22 | export const response = (ctx) => ctx.result -------------------------------------------------------------------------------- /webapp/src/resolvers/saveById.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils' 2 | 3 | export function request(ctx) { 4 | const values = ctx.arguments.input 5 | const keys = { id: ctx.args.input.id ?? util.autoId() } 6 | 7 | const timeStamp = util.time.nowISO8601() 8 | 9 | values.createdAt = ctx.args.input.createdAt ?? timeStamp 10 | values.updatedAt = timeStamp 11 | 12 | return { 13 | operation: 'PutItem', 14 | key: util.dynamodb.toMapValues(keys), 15 | attributeValues: util.dynamodb.toMapValues(values), 16 | } 17 | } 18 | 19 | export function response(ctx) { 20 | return ctx.result 21 | } -------------------------------------------------------------------------------- /webapp/src/resolvers/statusTrips.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | 3 | export function request(ctx) { 4 | console.log("ctx.identity:", JSON.parse(JSON.stringify(ctx.identity))) 5 | const { status = 'inroute', limit = 20, nextToken } = ctx.arguments; 6 | const index = 'statusSgi'; 7 | const query = JSON.parse( 8 | util.transform.toDynamoDBConditionExpression({ status: { eq: status } }) 9 | ); 10 | return { operation: 'Query', index, query, limit, nextToken }; 11 | } 12 | 13 | export function response(ctx) { 14 | console.log("ctx.identity:", JSON.parse(JSON.stringify(ctx.identity))) 15 | const { items: trips = [], nextToken } = ctx.result; 16 | return { trips, nextToken }; 17 | } 18 | 19 | 20 | // https://aws.amazon.com/blogs/mobile/appsync-pipeline-resolvers-2/ 21 | // https://docs.aws.amazon.com/appsync/latest/devguide/js-resolver-reference-dynamodb.html 22 | // https://docs.aws.amazon.com/appsync/latest/devguide/tutorial-dynamodb-resolvers-js.html 23 | // https://docs.aws.amazon.com/appsync/latest/devguide/configuring-resolvers-js.html 24 | // https://aws.amazon.com/blogs/mobile/introducing-new-aws-appsync-module-and-functions-for-dynamodb-javascript-resolvers/ 25 | // https://advancedweb.hu/first-experiences-with-the-new-appsync-javascript-resolver-runtime/ 26 | // https://levelup.gitconnected.com/slashing-serverless-api-latency-with-appsync-javascript-resolvers-8aa5ae6a9ac0 27 | // https://stefan-majiros.com/blog/custom-graphql-batchgetitem-resolver-in-aws-amplify-for-appsync 28 | // https://serverlessland.com/patterns/appsync-dynamodb-singletable-js-resolver 29 | // https://blog.graphbolt.dev/write-reusable-code-for-appsync-javascript-resolvers (create item) -------------------------------------------------------------------------------- /webapp/src/resolvers/updateById.js: -------------------------------------------------------------------------------- 1 | import { util } from '@aws-appsync/utils'; 2 | import * as ddb from '@aws-appsync/utils/dynamodb'; 3 | 4 | export function request(ctx) { 5 | const { id, ...rest } = ctx.args; 6 | const values = Object.entries(rest).reduce((obj, [key, value]) => { 7 | obj[key] = value ?? ddb.operations.remove(); 8 | return obj; 9 | }, {}); 10 | 11 | return ddb.update({ 12 | key: { id }, 13 | update: { ...values, version: ddb.operations.increment(1) }, 14 | }); 15 | } 16 | 17 | export function response(ctx) { 18 | const { error, result } = ctx; 19 | if (error) { 20 | util.appendError(error.message, error.type); 21 | } 22 | return result; 23 | } -------------------------------------------------------------------------------- /webapp/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import { useUserStore } from '../stores/user' 3 | import SimpleLayout from '../layouts/SimpleLayout.vue' 4 | import HomeView from '../views/HomeView.vue' 5 | import AuthView from '../views/AuthView.vue' 6 | import DriversView from '../views/DriversView.vue' 7 | import TripsView from '../views/TripsView.vue' 8 | 9 | const router = createRouter({ 10 | history: createWebHistory(import.meta.env.BASE_URL), 11 | routes: [ 12 | { 13 | path: "/", 14 | component: SimpleLayout, 15 | children: [ 16 | { 17 | path: '/', 18 | name: 'home', 19 | component: HomeView, 20 | meta: { requiresAuth: true } 21 | }, 22 | { 23 | path: "drivers", 24 | component: DriversView, 25 | meta: { requiresAuth: true } 26 | }, 27 | { 28 | path: "trips", 29 | component: TripsView, 30 | meta: { requiresAuth: true } 31 | } 32 | ] 33 | }, 34 | { 35 | path: "/auth", 36 | component: SimpleLayout, 37 | children: [ 38 | { 39 | path: "", 40 | name: "auth", 41 | component: AuthView, 42 | props: route => ({ ...route.params, ...route.query }), // converts query strings and params to props 43 | meta: { name: 'AuthView' } 44 | } 45 | ] 46 | } 47 | 48 | ] 49 | }) 50 | 51 | 52 | router.beforeEach(async (to) => { 53 | const store = useUserStore() 54 | if (to.matched.some(record => record.meta.requiresAuth)) { 55 | if (!store.isAuthenticated) { 56 | try { 57 | console.log("dispatch getSession"); 58 | await store.getSession(); 59 | return to.fullPath; 60 | } catch (err) { 61 | //console.log("router beforeEach Error: " + err); 62 | return '/auth' 63 | } 64 | } 65 | } 66 | }); 67 | 68 | export default router 69 | -------------------------------------------------------------------------------- /webapp/src/stores/geo.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { fetchAuthSession } from 'aws-amplify/auth'; 3 | import { LocationClient, PutGeofenceCommand, CalculateRouteCommand, ListGeofencesCommand, SearchPlaceIndexForTextCommand, SearchPlaceIndexForPositionCommand } from '@aws-sdk/client-location'; 4 | import { generateClient } from 'aws-amplify/api'; 5 | import * as mutations from "../graphql/mutations"; 6 | import * as queries from "../graphql/queries"; 7 | import circle from '@turf/circle' 8 | import { useUserStore } from "../stores/user"; 9 | import { ConsoleLogger } from 'aws-amplify/utils'; 10 | import { configAmplify } from "../configAmplify"; 11 | 12 | const logger = new ConsoleLogger('geotrack'); 13 | configAmplify(); 14 | 15 | const api_client = generateClient() 16 | 17 | const locationClient = async () => { 18 | const session = await fetchAuthSession(); 19 | const client = new LocationClient({ 20 | credentials: session.credentials, 21 | region: import.meta.env.VITE_AWS_REGION, 22 | }); 23 | return client; 24 | }; 25 | 26 | export const useGeoStore = defineStore("geo", { 27 | state: () => ({ 28 | userStore: useUserStore(), 29 | driversList: [], 30 | loading: false, 31 | paginationToken: "", 32 | driverRec: "", 33 | deviceRec: "", 34 | depCoord: [], 35 | destCoord: [], 36 | routeSummary: {}, 37 | routeSteps: [], 38 | geoFencePolygon: null, 39 | devicesIdsInRoute: [], 40 | geoFenceList: [] 41 | }), 42 | 43 | actions: { 44 | uniqueId() { 45 | const dateString = Date.now().toString(36); 46 | const randomness = Math.random() 47 | .toString(36) 48 | .slice(2); 49 | return dateString + randomness; 50 | }, 51 | 52 | ddbExpirationTime(days) { 53 | let date = new Date(); 54 | date.setDate(date.getDate() + days); 55 | return date; 56 | }, 57 | 58 | async fetchDevicesIdsInRoute() { 59 | try { 60 | let deviceIds = []; 61 | console.group("fetchDevicesIdsInRoute"); 62 | 63 | const results = await api_client.graphql({ 64 | query: queries.deviceIdByTripStatus, 65 | variables: { status: "inroute" }, 66 | //authToken: this.userStore.token 67 | }); 68 | 69 | for (let i = 0; i < results.data.statusTrips.trips.length; i++) { 70 | deviceIds.push(results.data.statusTrips.trips[i].driver.deviceId) 71 | } 72 | logger.info("Drivers in route: " + deviceIds.length); 73 | console.groupEnd(); 74 | return deviceIds; 75 | } catch (error) { 76 | logger.error(error); 77 | console.groupEnd(); 78 | throw error; 79 | } 80 | }, 81 | 82 | async saveGeoFence(name, polygonVertices) { 83 | let geoFenceId = null; 84 | try { 85 | console.group("saveGeoFence"); 86 | this.loading = true; 87 | 88 | const geoParams = { 89 | CollectionName: import.meta.env.VITE_GEOFENCE, 90 | GeofenceId: name, 91 | Geometry: { 92 | Polygon: [polygonVertices] 93 | } 94 | } 95 | 96 | const locationService = await locationClient(); 97 | const command = new PutGeofenceCommand(geoParams); 98 | const data = await locationService.send(command); 99 | 100 | if (data && data.GeofenceId) { 101 | logger.info("Saved on Amazon Location Service: " + data.GeofenceId); 102 | geoFenceId = data.GeofenceId 103 | } 104 | else { 105 | console.warn(data) 106 | } 107 | 108 | this.loading = false; 109 | console.groupEnd(); 110 | return geoFenceId 111 | 112 | } catch (error) { 113 | logger.error(error); 114 | this.loading = false; 115 | console.groupEnd(); 116 | throw error; 117 | } 118 | }, 119 | 120 | async calculateRoute(depLngLat = null, destLngLat = null) { 121 | if (depLngLat) { 122 | this.depCoord = depLngLat; 123 | } 124 | if (destLngLat) { 125 | this.destCoord = destLngLat; 126 | } 127 | 128 | console.group("calculateRoute"); 129 | var params = { 130 | CalculatorName: import.meta.env.VITE_GEOROUTE_CALCULATION, 131 | DeparturePosition: [ 132 | this.depCoord.lng, 133 | this.depCoord.lat, 134 | ], 135 | DestinationPosition: [ 136 | this.destCoord.lng, 137 | this.destCoord.lat, 138 | ], 139 | DepartNow: false, 140 | IncludeLegGeometry: true, 141 | TravelMode: 'Car' 142 | }; 143 | 144 | const locationService = await locationClient(); 145 | const command = new CalculateRouteCommand(params); 146 | const data = await locationService.send(command); 147 | 148 | if (data && data.Summary) { 149 | this.routeSteps = [...data.Legs[0].Geometry.LineString]; 150 | this.routeSummary = data.Summary; 151 | 152 | console.groupEnd(); 153 | return ({ "summary": data.Summary, "steps": [...data.Legs[0].Geometry.LineString] }); 154 | } 155 | else { 156 | console.warn(data) 157 | return ({ "summary": "", "steps": [] }); 158 | } 159 | 160 | }, 161 | 162 | async searchPlaceIndexForPosition(params) { 163 | const locationService = await locationClient(); 164 | const command = new SearchPlaceIndexForPositionCommand(params); 165 | const data = await locationService.send(command); 166 | 167 | if (data && response.Results.length > 0) { 168 | return data.Results[0].Place.Label 169 | } 170 | else { 171 | return [] 172 | } 173 | }, 174 | 175 | async searchPlaceIndexForText(params) { 176 | const locationService = await locationClient(); 177 | const command = new SearchPlaceIndexForTextCommand(params); 178 | const data = await locationService.send(command); 179 | 180 | if (data && data.Results.length > 0) { 181 | let placeOptions = [] 182 | for (var i = 0; i < data.Results.length; i++) { 183 | placeOptions.push({ 184 | title: data.Results[i].Place.Label, 185 | value: data.Results[i].Place.Geometry.Point, 186 | }); 187 | } 188 | return placeOptions 189 | } 190 | else { 191 | return [] 192 | } 193 | }, 194 | 195 | calculateGeoFence(center) { 196 | var options = { 197 | steps: 10, 198 | units: "kilometers", 199 | options: {}, 200 | }; 201 | var radius = 1; 202 | var polygon = circle(center, radius, options); 203 | return polygon.geometry.coordinates 204 | }, 205 | 206 | async fetchGeoFenceItems() { 207 | try { 208 | let geoFences = []; 209 | console.group("fetchGeoFenceItems"); 210 | this.loading = true; 211 | this.geoFenceList = []; 212 | 213 | const locationService = await locationClient(); 214 | const command = new ListGeofencesCommand({ CollectionName: import.meta.env.VITE_GEOFENCE }); 215 | const data = await locationService.send(command); 216 | 217 | if (data && data.Entries.length > 0) { 218 | for (let i = 0; i < data.Entries.length; i++) { 219 | if (data.Entries[i].Status == "ACTIVE") { 220 | geoFences.push({ 221 | id: data.Entries[i].GeofenceId, 222 | geoFenceName: data.Entries[i].GeofenceId, 223 | boundary: data.Entries[i].Geometry.Polygon 224 | }) 225 | } 226 | } 227 | } 228 | 229 | //logger.info(usersList); 230 | this.geoFenceList = geoFences; 231 | this.loading = false; 232 | console.groupEnd(); 233 | } catch (error) { 234 | logger.error(error); 235 | this.loading = false; 236 | console.groupEnd(); 237 | throw error; 238 | } 239 | }, 240 | 241 | async listTrips() { 242 | let tripsList = null 243 | try { 244 | console.group("listTrips"); 245 | this.loading = true; 246 | this.driversList = []; 247 | const tripResults = await api_client.graphql({ 248 | query: queries.listTrips, 249 | //authToken: this.userStore.token 250 | }); 251 | tripsList = [...tripResults.data.listTrips.trips] 252 | this.loading = false; 253 | console.groupEnd(); 254 | return tripsList; 255 | } catch (error) { 256 | logger.error(error); 257 | this.loading = false; 258 | console.groupEnd(); 259 | throw error; 260 | } 261 | }, 262 | 263 | async listDrivers() { 264 | let driversList = null 265 | try { 266 | console.group("listDrivers"); 267 | this.loading = true; 268 | this.driversList = []; 269 | const driverResults = await api_client.graphql({ 270 | query: queries.listDrivers, 271 | //authToken: this.userStore.token 272 | }); 273 | driversList = [...driverResults.data.listDrivers.drivers] 274 | this.loading = false; 275 | console.groupEnd(); 276 | return driversList; 277 | } catch (error) { 278 | logger.error(error); 279 | this.loading = false; 280 | console.groupEnd(); 281 | throw error; 282 | } 283 | }, 284 | 285 | async saveDriver(driverRecord) { 286 | try { 287 | console.group("saveDriver"); 288 | this.loading = true; 289 | let result = ""; 290 | 291 | var driverInput = { 292 | fullName: driverRecord.fullName, 293 | email: driverRecord.email, 294 | deliveryType: driverRecord.deliveryType, 295 | deviceType: driverRecord.deviceType, 296 | deviceId: driverRecord.deviceId, 297 | } 298 | 299 | if (driverRecord.id != null && driverRecord.id.length > 2) { 300 | driverInput["id"] = driverRecord.id 301 | } 302 | 303 | if (driverRecord.status != null && driverRecord.status.length > 2) { 304 | driverInput["status"] = driverRecord.status 305 | } else { 306 | driverInput["status"] = "active" 307 | } 308 | 309 | 310 | result = await api_client.graphql({ 311 | query: mutations.saveDriver, 312 | variables: { input: driverInput }, 313 | //authToken: this.userStore.token 314 | }); 315 | 316 | this.loading = false; 317 | this.driverRec = result; 318 | console.groupEnd(); 319 | return result; 320 | } catch (error) { 321 | logger.error(error); 322 | this.loading = false; 323 | console.groupEnd(); 324 | throw error; 325 | } 326 | }, 327 | 328 | async saveTrip(tripRecord) { 329 | try { 330 | console.group("saveTrip"); 331 | this.loading = true; 332 | let result = ""; 333 | let route = null; 334 | 335 | logger.info("calculating routing") 336 | route = await this.calculateRoute() 337 | 338 | this.geoFencePolygon = this.calculateGeoFence([this.destCoord.lng, this.destCoord.lat]); 339 | 340 | const geoFenceId = await this.saveGeoFence( 341 | this.uniqueId(), 342 | this.geoFencePolygon[0] 343 | ); 344 | 345 | if (geoFenceId == null) { 346 | logger.error("Error saving geoFence") 347 | return; 348 | } 349 | 350 | var tripInput = { 351 | labelStart: tripRecord.trip.labelStart, 352 | geoStart: this.depCoord, 353 | labelEnd: tripRecord.trip.labelEnd, 354 | geoEnd: this.destCoord, 355 | distance: Math.round(route.summary.Distance).toString(), 356 | duration: Math.round( 357 | route.summary.DurationSeconds / 60 358 | ).toString(), 359 | geoFenceId: geoFenceId, 360 | //expireAt: Math.round(this.ddbExpirationTime(7).getTime()), // Expire the date 7 days from today - DynamoDb Expire 361 | status: "accepted", 362 | driver: { 363 | id: tripRecord.trip.driver.id 364 | }, 365 | clientPhone: tripRecord.trip.clientPhone, 366 | } 367 | 368 | result = await api_client.graphql({ 369 | query: mutations.saveTrip, 370 | variables: { input: tripInput }, 371 | //auth: this.userStore.token 372 | }); 373 | 374 | this.loading = false; 375 | console.groupEnd(); 376 | return result; 377 | } catch (error) { 378 | logger.error(error); 379 | } 380 | }, 381 | 382 | async delDriver(id) { 383 | try { 384 | console.group("delDriver"); 385 | this.loading = true; 386 | let result = ""; 387 | 388 | result = await api_client.graphql({ 389 | query: mutations.delDriver, 390 | variables: { id: id }, 391 | //authToken: this.userStore.token 392 | }); 393 | 394 | this.loading = false; 395 | console.groupEnd(); 396 | return result; 397 | 398 | } catch (error) { 399 | logger.error(error); 400 | this.loading = false; 401 | console.groupEnd(); 402 | throw error; 403 | } 404 | }, 405 | 406 | async delTrip(id) { 407 | try { 408 | console.group("delTrip"); 409 | this.loading = true; 410 | let result = ""; 411 | 412 | result = await api_client.graphql({ 413 | query: mutations.delTrip, 414 | variables: { id: id }, 415 | //authToken: this.userStore.token 416 | }); 417 | 418 | this.loading = false; 419 | console.groupEnd(); 420 | return result; 421 | 422 | } catch (error) { 423 | logger.error(error); 424 | this.loading = false; 425 | console.groupEnd(); 426 | throw error; 427 | } 428 | }, 429 | 430 | }, 431 | }); -------------------------------------------------------------------------------- /webapp/src/stores/user.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; 2 | import { fetchUserAttributes, getCurrentUser } from 'aws-amplify/auth'; 3 | import { ConsoleLogger } from 'aws-amplify/utils'; 4 | import { configAmplify } from "../configAmplify"; 5 | 6 | const logger = new ConsoleLogger('geotrack'); 7 | configAmplify(); 8 | 9 | export const useUserStore = defineStore("user", { 10 | state: () => ({ 11 | user: null, 12 | userAttributes: null, 13 | userTokens: null 14 | }), 15 | 16 | getters: { 17 | isAuthenticated: (state) => !!state.user, 18 | userId: (state) => state.userAttributes?.sub, 19 | email: (state) => state.userAttributes?.email, 20 | fullname: (state) => state.userAttributes?.given_name + " " + state.userAttributes?.family_name, 21 | }, 22 | actions: { 23 | async getSession() { 24 | try { 25 | this.user = await getCurrentUser(); 26 | this.userAttributes = await fetchUserAttributes(); 27 | console.groupEnd(); 28 | } catch (err) { 29 | console.error(err); 30 | console.log("isAuthenticated: " + !!this.user); 31 | throw new Error(err); 32 | } 33 | }, 34 | }, 35 | }); -------------------------------------------------------------------------------- /webapp/src/views/AboutView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /webapp/src/views/AuthView.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | -------------------------------------------------------------------------------- /webapp/src/views/DriversView.vue: -------------------------------------------------------------------------------- 1 | 118 | 119 | 236 | -------------------------------------------------------------------------------- /webapp/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 21 | 22 | -------------------------------------------------------------------------------- /webapp/src/views/TripsView.vue: -------------------------------------------------------------------------------- 1 | 232 | 233 | 391 | -------------------------------------------------------------------------------- /webapp/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [ 7 | vue(), 8 | ], 9 | resolve: { 10 | alias: [ 11 | { 12 | find: './runtimeConfig', 13 | replacement: './runtimeConfig.browser', 14 | } 15 | ] 16 | } 17 | }) -------------------------------------------------------------------------------- /webappconfig.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DARKGRAY='\033[1;30m' 4 | RED='\033[0;31m' 5 | LIGHTRED='\033[1;31m' 6 | GREEN='\033[0;32m' 7 | YELLOW='\033[1;33m' 8 | BLUE='\033[0;34m' 9 | PURPLE='\033[0;35m' 10 | LIGHTPURPLE='\033[1;35m' 11 | CYAN='\033[0;36m' 12 | WHITE='\033[1;37m' 13 | SET='\033[0m' 14 | DATESUFFIX=$(date +%Y-%m-%d-%H%M) 15 | 16 | STACKNAME=$(cat samconfig.toml | grep stack_name | tr -d '"' | awk '{print $3}') 17 | AWSREGION=$(cat samconfig.toml | grep region | tr -d '"' | awk '{print $3}') 18 | 19 | if [ -z "$STACKNAME" ]; then 20 | echo -e "${RED} Stack name could not be found at the samconfig.toml file ${RED}${SET} - Fail" 21 | exit 1 22 | fi 23 | 24 | if [ -z "$AWSREGION" ]; then 25 | echo -e "${RED} AWS Region could not be found at the samconfig.toml file ${RED}${SET} - Fail" 26 | exit 1 27 | fi 28 | 29 | echo -e "${WHITE}Stack name: ${YELLOW}$STACKNAME${SET}" 30 | echo -e "${WHITE}AWS Region: ${YELLOW}$AWSREGION${SET}" 31 | 32 | function get_output() { 33 | local RSP=$(aws cloudformation describe-stacks --stack-name $STACKNAME --query "Stacks[0].Outputs[?OutputKey=='$1'].OutputValue" --output text --region $AWSREGION) 34 | eval "echo $1: $RSP" 35 | eval "$1=$RSP" 36 | } 37 | 38 | get_output ApiId 39 | get_output AwsRegion 40 | get_output AppSyncUrl 41 | get_output CloudFrontUrl 42 | get_output IdentityPoolId 43 | get_output CognitoUserPoolId 44 | get_output CognitoUserPoolClientId 45 | get_output CognitoDomainName 46 | get_output S3WebAppBucket 47 | get_output GeoMap 48 | get_output GeoRouteCalculation 49 | get_output GeoPlaceIndex 50 | get_output GeoTracker 51 | get_output GeoFence 52 | 53 | echo -e "-- Creating .env file" 54 | 55 | echo "VITE_API_URL=https://${ApiId}.execute-api.${AwsRegion}.amazonaws.com/dev" > webapp/.env 56 | echo "VITE_AWS_REGION=${AwsRegion}" >> webapp/.env 57 | echo "VITE_CLOUDFRONT_URL=https://${CloudFrontUrl}" >> webapp/.env 58 | echo "VITE_COGNITO_USER_POOL_CLIENT_ID=${CognitoUserPoolClientId}" >> webapp/.env 59 | echo "VITE_COGNITO_USER_POOL_ID=${CognitoUserPoolId}" >> webapp/.env 60 | echo "VITE_COGNITO_DOMAIN=${CognitoDomainName}.auth.${AwsRegion}.amazoncognito.com" >> webapp/.env 61 | echo "VITE_IDENTITY_POOL_ID=${IdentityPoolId}" >> webapp/.env 62 | echo "VITE_BUCKET_NAME=${S3WebAppBucket}" >> webapp/.env 63 | 64 | echo "VITE_GEOMAP=${GeoMap}" >> webapp/.env 65 | echo "VITE_GEOROUTE_CALCULATION=${GeoRouteCalculation}" >> webapp/.env 66 | echo "VITE_GEOPLACE_INDEX=${GeoPlaceIndex}" >> webapp/.env 67 | echo "VITE_GEOTRACKER=${GeoTracker}" >> webapp/.env 68 | echo "VITE_GEOFENCE=${GeoFence}" >> webapp/.env 69 | echo "VITE_GRAPHQL_ENDPOINT=${AppSyncUrl}" >> webapp/.env 70 | 71 | cd webapp 72 | npm install 73 | npm run build 74 | aws s3 cp dist "s3://${S3WebAppBucket}" --recursive 75 | cd .. 76 | 77 | echo -e "- Web App available at: ${YELLOW}https://${CloudFrontUrl}${SET}" 78 | --------------------------------------------------------------------------------