1. Capturing and Processing Camera Images using OpenCV on Raspberry Pi (or elsewhere) with Python: The central component of this project involves capturing images from a camera, followed by using the OpenCV library for image analysis. This image processing can be performed either on the Raspberry Pi device itself or on another suitable platform, utilizing Python with OpenCV for image analysis and processing. This advanced technology enables the system to accurately detect the presence of vehicles in the parking area and count them.
2. Analysis of Available Parking Spaces: OpenCV analysis allows the project to identify vacant parking spaces based on image data, providing real-time representation of the number of available parking spots.
3. Web Application: The project also encompasses the development of a web application using Vue.js and Node.js for the frontend and backend, respectively. Through this application, users can access live video feeds from the parking camera, view the number of available and total parking spaces, and obtain information about the parking location.
4. PostgreSQL Database: All relevant data, including the count of available and total parking spaces, as well as the coordinates of parking locations, is stored in a PostgreSQL database. This ensures a reliable and persistent data archive.
5. Integration with MapBox: The project utilizes MapBox technology to create an interactive map in both the web and Android applications. All parking areas are marked on the map, and clicking on them displays information about the number of available and total parking spaces.
6. Android Application: In addition to the web application, there is an Android application that allows users to find parking spaces using the MapBox map and receive navigation instructions to their selected parking spot.
User Experience and Navigation: Through this project, drivers can easily find vacant parking spaces and receive navigation directions to them, reducing stress and saving time. The Android application, in particular, stands out by providing a straightforward way for users to select their desired parking spot and obtain route guidance to reach it.
Conclusion: "CV-Park Vision" is an ambitious project that leverages advanced technology, particularly OpenCV, to transform the parking experience. It offers precise, real-time information about available parking spaces, helping drivers save time and frustration. Additionally, the interactive map and Android application make this project accessible and valuable to a wide range of users.
-----------------------------------------------------------------------------------------------------------------
PHASE 2: Develop and ImplementIn phase 1 of this project, it is briefly explained what will be built. Below this text is an image representing the implementation of this system called CVPark Vision.
At the top of the list is the BalenaFin hardware component with Raspbian operating system. BalenaFin will receive a model from the Roboflow and OpenCV platforms that it will be able to use in a Python script to detect cars in the image. (Video frame)
The script will count the cars and save the fresh information about the number of cars for the corresponding parking lot in the PostgreSQL database.
Also using Flask, the image (video frame) will stream to a local address with a specific port so that the image (video frame) can be used in the Web Application.Web and Android applications retrieve data from the PostgreSQL database and present them to application users on maps (Mapbox).
The Android application is connected with Google Maps Navigation, and if the user wants it, it will take him to the desired parking lot. In addition to the map, the web application has the option of live viewing of the image (Video frame) from the parking lot.
Web application is intended for parking control and monitoring (City administration), and Android application is intended for end users who are looking for parking.
Step 1: Training model with RoboflowYou need to use one of the model training platforms for car recognition. The Roboflow platform offers 10 free training credits to OpenCV AI Competition users upon registration. (Thanks for the 10 training credits) I have used these 10 training credits to train my car recognition model. You need to create an account on the RoboFlow platform and start training. The steps are described below.
Creating a Workspace and Project on Roboflow platformDuring registration, you will be offered the option to create your first Workspace. Follow the instructions and create it according to your preferences.
After creating the workspace, you can now create a project by selecting the workspace you've created and, within it, clicking on Create New Project (Button is located in the top right corner).
Popup will open and asking you to choose the type of project you want to use, provide a name for the project, and specify the objects you want to recognize. Here, select "Object Detection, " enter the name of your project, and specify the objects you want to recognize (in our case, it's cars).
After filling in all the fields, select Create Public Project.
Once the project is created, you can add images or video clips on which you want to perform object detection (in our case, cars).
The more images and video clips you add, the more accurate your model will be.
Labeling Cars in Images and Generating a DatasetAfter adding and saving the images, you will be redirected to the Annotate menu. Here, you annotate or mark the objects (cars) in the images.
NOTE: Link to Video used in this project. (Free to use). I made changes on video so that it could be used (cropped, replayed several times to make it last longer)
You need to add each image to one of the datasets, and there are three: Valid (20%), Train (80%), and Test (10%).
Once all images have been annotated with car markings, you can now proceed to generate a new version of the model, which will be trained on the entered and annotated data.
Before the actual generation, you can choose various image processing options for the ones you have marked (changing orientation, flipping, black and white conversion, noise, resize, etc.).
After all the parameters are chosen and the generation process is initiated, it will take some time depending on the quantity of photos used for training the model.
We can see on the left side that there are versions of the generated (trained) models. This process can be repeated to achieve better results by adjusting parameters.
After the first version of the model has been trained, we can now use it to recognize cars in other images so that the model learns as much as possible.
When we load new images, we go to Annotate and select the images we want to train. After the image opens, select "Label Assist" in the menu on the right. We will be offered a window in which we can select the project and its version based on which we want to train the image.
In the upper right corner, you can adjust the detection precision parameters, and also manually correct as needed.
The more times the model is trained with more data, the more accurate it will be in detecting cars in the parking lot. The second version of my project was trained on a set of 70+ photos including the first version which had about 25 photos.
Training on a model of 70+ photos takes 2+ hours. The parameters from the image below need to be as close as possible to 100%, as this is the ideal value for car detection.
Final version of my project was trained on a set 96 photos. (version 3) This version is include in Python script.
After the model is trained, we can start using it for our needs.
NOTE: If you need to know
- model name: car-detection-on-parking
- model version: 1,2 and 3
Data can be exported by selecting Export Dataset in the top right corner and choosing the desired format. It is also possible to use the Roboflow API to retrieve a trained dataset and use it in the code.
After the model has been trained using RoboFlow, we can test the model through a Python script, for example, using a single image. The image contains several cars in a parking lot with a few vacant spaces. We will perform car detection and print the number of cars present. A Python script has been used that connects directly to the RoboFlow project via an API.
from inference.models.utils import get_roboflow_model
import cv2
# Image path
image_path = "park.png"
# Roboflow model
model_name = "model_name"
model_version = "model_version"
# Get Roboflow parking detection model
model = get_roboflow_model(
model_id="{}/{}".format(model_name, model_version),
# Replace ROBOFLOW_API_KEY with your Roboflow API Key
api_key="roboflow_api_key"
)
# Load image with opencv
frame = cv2.imread(image_path)
# Inference image to find cars
results = model.infer(image=frame, confidence=0.3, iou_threshold=0.5)
# Count the total number of cars
total_cars = len(results[0])
# Iterate through all detected cars
for car_result in results[0]:
bounding_box = car_result[:4]
print(bounding_box)
x0, y0, x1, y1 = map(int, bounding_box)
# Draw bounding box
cv2.rectangle(frame, (x0, y0), (x1, y1), (0, 0, 255), 2)
# Display the total number of cars in the top-left corner
text_size = cv2.getTextSize(f"Total Cars: {total_cars}", cv2.FONT_HERSHEY_SIMPLEX, 0.7, 2)[0]
cv2.rectangle(frame, (10, 10), (15 + text_size[0], 40), (0, 0, 0), -1)
cv2.putText(frame, f"Total Cars: {total_cars}", (15, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2)
# Show image
cv2.imshow('Original Frame', cv2.imread(image_path))
cv2.imshow('Result Frame', frame)
cv2.waitKey(0)
cv2.destroyAllWindows()
NOTE: The complete script are attached with the names: testModel.py
Code created based on the Roboflow docs and blog.
At the beginning of the script, it is necessary to modify the attributes: model_name, model_version, and api_key.
model_name (red box) I model_version (green box) can be found in the "Versions" option from the menu and read as shown in the image:
api_key can be found by following the instructions in the image below. It is a private API key that you add to your Python script.
After running the Python script, the results can be seen in the following image. The execution takes a few seconds.
To improve car detection accuracy, it is necessary to train the model (dataset) with a larger number of samples. (Image/Video)
Two cars are not in the parking spaces and these cars are not detected (They are marked in green in the second picture through the photo editor so that you can notice them)
Step 2: Setup and Create PostgreSQL databaseTo install PostgreSQL on your Linux Ubuntu system, you can use the following commands:
sudo apt update
sudo apt install postgresql postgresql-contrib
After installation, PostgreSQL will run as a service, and the default user will be "postgres." To set a password for this user, you can use the following command:
sudo password postgres
To connect to the PostgreSQL database, use the following command:
sudo -u postgres psql
Creating a Database and TableFirst, let's create a new database. You can do this with the following SQL command:
CREATE DATABASE SMARTPARKING;
To view a list of all created databases, you can use the following command within the PostgreSQL interactive terminal (psql):
\l
To open the database you've created, you can use the following command:
\c SMARTPARKING;
This command connects to the "SMARTPARKING" database. Make sure you execute it after launching the PostgreSQL interactive terminal using the psql
command and when you want to work with the specific database you've created.
After creating the database, we will now create a table called "PARKINGS" to store information about parking lots:
CREATE TABLE PARKINGS (
ID serial PRIMARY KEY,
PARKING_NAME VARCHAR(255),
LATITUDE VARCHAR(255) NOT NULL,
LONGITUDE VARCHAR(255) NOT NULL,
PARKING_FEE NUMERIC(10, 2),
PARKING_COST NUMERIC(10, 2),
FREE_SPACE INTEGER,
TOTAL_SPACE INTEGER,
DATE_UPDATE TIMESTAMP NOT NULL,
CITY VARCHAR(255)
);
Column descriptions:
- ID: Unique identifier for each parking lot
- PARKING_NAME: Name of the parking lot
- LATITUDE: Geographic latitude of the parking lot
- LONGITUDE: Geographic longitude of the parking lot
- PARKING_FEE: Parking fee (1 - with charge, 0 - free)
- PARKING_COST: Parking cost (Number with two decimal places)
- FREE_SPACE: Number of available parking spaces
- TOTAL_SPACE: Total number of parking spaces
- DATE_UPDATE: Date and time of the last data update
- CITY: City where the parking lot is located
To add data to the table, you can use the SQL "INSERT INTO" command. For example:
INSERT INTO PARKINGS
(PARKING_NAME,
LATITUDE,
LONGITUDE,
PARKING_FEE,
PARKING_COST,
FREE_SPACE,
TOTAL_SPACE,
DATE_UPDATE,
CITY)
VALUES
('Parking Sarajevo III',
'44.13114051595557',
'18.11927489077046',
'5.00',
'10.00',
5,
15,
'30-08-2023 23:17:40',
'Sarajevo');
Viewing Data from a TableTo retrieve data from a table in PostgreSQL, you can use the following command within the PostgreSQL interactive terminal (psql):
SELECT * FROM PARKINGS;
The balenaFin is a Raspberry Pi Compute Module carrier board that can run all the software that the Raspberry Pi can run, but hardened for deployment in the field.
In the project will using:
- balenaFin v1.0 board, which has 8GB of storage memory and 1GB of RAM
- Raspberry Pi Compute Model 3+ BCM2837 processor and 1GB RAM
Will be using the Raspbian operating system. (Details for installing and setup is on link. However, a brief explanation is below.)
From the Raspberry Pi website, we can download the Imager, which will facilitate the installation of the system on the board. Download the application and install it based on the operating system you are using on your computer.
After the application is installed, we need to:
- Before powering on the Fin, connect a USB to Micro-USB cable between your system and the Fin's USB_DBG port.
- Now, connect power to the Fin, either using the Phoenix or Barrel connector (Do not connect power to both!).
- Open Imager application
- Select Raspberry Pi Device (Must be 64-bit), Operating System and Storage.
You can connect the BalenaFin board via the HDMI port to a monitor, and you can also connect a keyboard and mouse to use it in that manner. It is possible to use VNC or SSH commands through the Linux Terminal as well. Use whichever method is more convenient for you. In my case, I utilized component connections to the board and VNC. Additionally, the terminal commands are straightforward, and we will demonstrate those as well.
First, after the system boots up on the board, you need to open the Terminal and configure the config.txt file so that the board can use the WiFi network, or you can connect it using a LAN cable without these modifications.
Open terminal on BalenaFin board and navigate to the "boot" directory.
cd /boot/
Execute the command to edit the config.txt file.
sudo nano config.txt
At the end of the file, add line:
dtoverlay=balena-fin
Press the keys Ctrl + X, and then Enter to save the changes. Restart the BalenaFin board; you can do this with the command:
reboot
After the board boots up, connect to the WiFi network. You can do this through the menu in the upper right corner for network connections.
You will need to do this either by connecting an Ethernet cable or by linking with computer components.
To enable VNC and SSH access through the Linux terminal, open the Raspberry Pi Configuration.
From the menu, select "Interfaces, " then activate SSH and VNC using the switches. After that, restart the board.
Install VNC Viewer on your computer and access your BalenaFin device using the IP address it uses on the network. When logging in, it will prompt you for login credentials, including a username and password.
For access through the Linux terminal using the SSH protocol, we use the following command:
ssh user@ip_addres
'user' in the command above is the username on your BalenaFin board (for Raspberry Pi, it is usually 'pi'), and 'ip_address' is the device's address on the local network.
With this, we have configured the environment on the BalenaFin board, and now we can proceed with the installation of the necessary tools and libraries for project.
Setting Up Python Environment and Installing Python LibrariesAt the beginning, it is necessary to install Python if it is not already present on the machine where the script will be running. The installation of Python is done with the following command:
sudo apt-get install python3
After Python is installed, you need to install several libraries that will be used in the script we are about to create:
- opencv-python is a Python package providing access to the OpenCV (Open Source Computer Vision) library, a popular tool for computer vision and image processing tasks. It offers various features such as object detection, motion tracking, face recognition, camera calibration, image segmentation, and more.To install opencv-python, use the following command:
sudo pip3 install opencv-python
- Flask is a Python web framework that facilitates the creation of web applications with ease. To install Flask, use the following command:
sudo pip3 install Flask
- Inference Python library supports running object detection, classification, instance segmentation, and even foundation models (like CLIP and SAM). You can train and deploy your own custom model or use one of the 50, 000+ fine-tuned models shared by the community:
sudo pip3 install inference
- psycopg2 is a Python adapter for working with PostgreSQL databases. This library enables Python developers to connect to PostgreSQL databases, execute SQL queries, transfer data between the database and Python applications, and manage transactions.To install psycopg2, you can use the following command:
sudo apt-get install libpq-dev
sudo pip3 install psycopg2
Now that all the libraries are set up and ready for use, we can proceed to create a Python script that will be used for detecting cars in the parking lot in this project.
Step 4: Python ScriptsNOTE: The complete scripts are attached with the names: m
ain.py
and util.py
.
The Python script implemented on the BalenaFin board needs to perform a series of tasks. At the beginning of the script, libraries and modules that will be used throughout it are defined.
# Import necessary libraries and modules
from inference.models.utils import get_roboflow_model
import cv2
import numpy as np
import psycopg2
from flask import Flask, Response
from util import update_parkings_table
These imports allow access to the functionalities provided by these libraries and the functions defined in other modules (such as get_roboflow_model
and updateParkingsTable
). Following these imports, there are definitions of other variables, constants, and functions that will be used in the script.
# Initialize Flask application
app = Flask(__name__)
# Define Roboflow model details
model_name = "model_name"
model_version = "model_version"
# Get the Roboflow model using the provided utility function
model = get_roboflow_model(model_id="{}/{}".format(model_name, model_version),
# Replace ROBOFLOW_API_KEY with your Roboflow API Key
api_key="Roboflow_API")
# Define parking location details
parking_latitude = "44.12897164875866"
parking_longitude = "18.117705446176494"
total_space = 69
Also, at the beginning, it is necessary to load a video or frame from a camera connected to BalenaFin. In this project, a video is used because there was no access to a physical parking lot or surveillance camera that could provide live video from the parking area. Therefore, the video is used as a "live stream" from the camera, and connecting the camera to the BalenaFin board is not difficult to capture images for analysis instead of these video frames.
NOTE: If use Pi Camera, use the PiCamera Python library and load a frame from the camera so that you can run it through the model. (Image is captured from the Pi camera and processed, instead of 'playing' a video as used in the example for this project.)
# Set the path for the video file
video_path = "video1.mp4"
# Open the video file
cap = cv2.VideoCapture(video_path)
# Check if the video opened successfully
if not cap.isOpened():
print("Error: Couldn't open video.")
exit()
Now we can proceed to writing the main function that will generate a video frame with marked cars and information about the number of car. This function is extensive, so will be describe individual parts, and the entire code is included in the attachment.
Function for Generate Video FramesAt the beginning of the function, an infinite loop is set up, which will only be terminated if there are no more frames from the video. For system with camera, this would occur if there is no video feed coming from the camera.
# Define a function to generate video frames
def generate_frames():
while True:
# Read a frame from the video
ret, frame = cap.read()
# Break the loop if the end of the video is reached
if not ret:
break
Perform car detection using the Roboflow model:
results = model.infer(image=frame, confidence=0.85, iou_threshold=0.9)
Note: "model" defined on top of script.
The confidence score is a measure of how confident the model is in its prediction. It represents the likelihood that a given prediction is correct. Typically, this score ranges from 0 to 1, where 1 indicates high confidence.
IOU_threshold is a metric used to evaluate the accuracy of an object detection algorithm. It measures the overlap between the predicted bounding box and the ground truth bounding box.
Get the total number of detected cars and create free_space value:
total_cars = len(results[0])
free_space = total_space-total_cars
Draw bounding boxes around detected cars (red box) with OpenCV Python library:
for car_result in results[0]:
bounding_box = car_result[:4]
x0, y0, x1, y1 = map(int, bounding_box)
cv2.rectangle(frame, (x0, y0), (x1, y1), (0, 0, 255), 2)
Display the total number of detected cars on the frame:
text_size = cv2.getTextSize(f"Free Space: {free_space}/{total_space}", cv2.FONT_HERSHEY_SIMPLEX, 0.5, 2)[0]
cv2.rectangle(frame, (10, 10), (15 + text_size[0], 40), (0, 0, 0), -1)
cv2.putText(frame, f"Free Space: {free_space}/{total_space}", (15, 35), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 2)
Convert the frame to JPEG format and convert JPEG to bytes format:
_, buffer = cv2.imencode('.jpg', frame)
frame_bytes = buffer.tobytes()
Use yield to send the frame via HTTP response
yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')
Update the parking table with the total number of detected cars (Function update_parkings_tableimplement in util.py script):
update_parkings_table(parking_latitude, parking_longitude, total_space, free_space)
Define a route for the video feed:
@app.route('/') def video_feed():
return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')
This sets up a route in the Flask application. When a user accesses a specific address (in this case, '/'), it triggers the video_feed() function, which returns an HTTP response of type 'multipart/x-mixed-replace'. This allows for continuous updating of images (frames) on the web application.
Run the Flask application:
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8000, debug=True)
This part of the code runs the Flask application, enabling it to listen on a specific port on all available network interfaces (0.0.0.0). Once the application is running, it can be accessed at the address http://localhost:8000/ to view the continuously generated frames.
This address is used when viewing the video frames locally on the machine where the script is running. In the case of viewing from another device on the local network, the IP address of the device running the script is used. For example, if the script is running on a device named "balenaFin, " the URL might look like:
http://<balenaFin-IP>:8000/
Replace <balenaFin-IP>
with the actual IP address of the "balenaFin" device. This allows you to access the Flask application from other devices in the local network. (Used in the Web Application section)
This function is located in the util.py script.
At the beginning of the util script, it is necessary to import libraries, add information for connecting to the database, and establish a connection. (This will be used in both functions from util.py script)
from datetime import datetime
import psycopg2 #Uvoz biblioteke za rad sa bazom podataka PostgreSQL
#Information for PostgreSQL database
dbname = "smartparking" # Database name
user = "postgres" # User for login on PostgreSQL
password = "postgres" # User for login on PostgreSQL
host = "192.168.0.14" # The IP address of the device hosting the running PostgreSQL database
port = "5432" #Port of PostgreSQL database
conn = psycopg2.connect(
dbname=dbname,
user=user,
password=password,
host=host,
port=port
)
Function get_parking_status retrieves information about available parking spaces from the PARKINGS table in the database.
Parameters:
- parkingLatitude: Latitude of the parking location.
- parkingLongitude: Longitude of the parking location.
Returns:
- The number of available parking spaces. (free_space)
def get_parking_status(parkingLatitude, parkingLongitude):
# SQL query to select the FREE_SPACE column based on lat and long
queryGet = "SELECT FREE_SPACE FROM PARKINGS WHERE LATITUDE = %s and LONGITUDE = %s;"
# Execute the query with the provided parameters
cur.execute(queryGet, (parkingLatitude, parkingLongitude))
# Fetch the result and get the value from the first row
result = cur.fetchall()
resultRow = result[0]
# Return the number of available parking spaces
return resultRow[0]
This function retrieves information about available parking spaces from the PARKINGS table in the database based on the provided latitude and longitude. The SQL query is executed, and the result is fetched. The function returns the number of available parking spaces.
This function will be used in the following function to update information about the current number of available parking spaces. If the number of available spaces is equal to the current number of free spaces, the update function will not make changes to the table.
Function for Update Parkings TableThis function is located in the util.py script.
Updates the PARKINGS table with new parking space information if there is a change.Parameters:
- parkingLatitude: Latitude of the parking location.
- parkingLongitude: Longitude of the parking location.
- totalSpace: Total number of parking spaces.
- freeSpace: Current number of available parking spaces.
def update_parkings_table(parkingLatitude, parkingLongitude, totalSpace, freeSpace):
# SQL query to update the TOTAL_SPACE, FREE_SPACE, and DATE_UPDATE columns
queryUpdate = "UPDATE PARKINGS SET TOTAL_SPACE = %s, FREE_SPACE = %s, DATE_UPDATE = %s WHERE LATITUDE = %s and LONGITUDE = %s;"
try:
# Get the current free space from the table
freeSpaceFromTable = getFromParkingsTable(parkingLatitude, parkingLongitude)
# Check if the free space has changed
if freeSpaceFromTable != freeSpace:
# Get the current date and time
dateUpdateNow = datetime.now()
dateUpdate = dateUpdateNow.strftime("%d-%m-%Y %H:%M:%S")
# Execute the update query with the new values
cur.execute(queryUpdate, (totalSpace, freeSpace, dateUpdate, parkingLatitude, parkingLongitude))
# Commit the changes to the database and close connection
conn.commit()
except Exception as e:
print(f"Error: Update table failed! {e}")
This function updates the PARKINGS table with new parking space information if there is a change. It compares the current number of available parking spaces with the stored value. If there is a difference, it updates the TOTAL_SPACE, FREE_SPACE, and DATE_UPDATE columns. The function uses the get_parking_stats function to obtain the current number of available parking spaces for comparison. If the values differ, it executes the update query and commits the changes to the database. If an exception occurs during the process, it prints an error message indicating that the update table operation failed.
Step 5: Web Application - DashboardIn this section, will be extensively implement a dashboard for smart parking. Will be explain how to install the necessary libraries and packages for developing a Vue.js and Node.js web application, as well as how to transfer images from the Python part of the system to the web application and display them. We will also explore how to retrieve and display parking status information from a PostgreSQL database on the dashboard. An overview of the data will be displayed on a map, showing the total number of parking spots and the number of available spaces. This section of the work will provide insight into the process of creating a central monitoring tool for parking, enabling efficient real-time parking monitoring.
Setting up environment and installing librariesFor the implementation of the web application, as mentioned earlier, will be use Vue.js and Node.js. Vue.js will be used for the user interface (frontend) part of the application, while Node.js will be used for the implementation of the server-side (backend) part of the application.
It is necessary to install the Node.js environment for the needs of this application. The installation can be done through the terminal using the command:
sudo apt update
sudo apt install nodejs
After the installation is complete, you can verify if Node.js has been successfully installed by using the command node -v. You should expect an output in the form of v19.0.0. npm (Node Package Manager) comes with Node.js and does not need to be separately installed.
To install Vue CLI (Command Line Interface) globally, use the following command:
sudo npm install -g @vue/cli
To initialize a new Vue.js project, you need to navigate to the location where you want to save the project through the terminal, and then create the project using the following command:
vue create project-name
The terminal will guide you through the setup, allowing you to choose options that suit the project you are implementing, such as whether to use ESLint, Babel, and similar tools. Once the project setup is complete, you can navigate into the project folder using the command:
cd project-name
Inside the folder, you will find two separate folders for the backend and frontend parts of the application. The folders have these names.
In the backend folder, the server-side of the application is implemented using Node.js. Here, HTTP requests are handled, communication with the database is established, and the server-side application logic is executed. To run the server-side part of the application, you need to navigate to the backend folder of the application and use the command:
npm run dev
In the frontend folder, you will find the Vue.js application that serves as the user interface of the application. This is where everything related to what users see is implemented, such as components, routes, styles, and more. To run the user interface of the application, you need to navigate to the frontend folder of the application and use the command:
npm run serve
This project organization approach allows for maintaining a clean and clear code structure, making development easier. Frontend and backend are separated, making the application scalable and flexible for future changes and upgrades.
Within the Vue.js project folder, you can install additional libraries using npm. To handle HTTP requests, the Axios library is used. This library is necessary for retrieving an image from the Python script that processes the video. You can install the Axios library using the command:
npm install axios
To establish a connection to a PostgreSQL database from Node.js, you can use the pg library (PostgreSQL for Node.js). This library can be installed using the command:
npm install pg
The last library required for the web application is the connection to the MapBox platform. The mapbox-gl library contains interactive maps and allows you to work with maps. You can install this library using the command:
npm install mapbox-gl
Reading data from a PostgreSQL databaseNOTE:Service for reading data from a PostgreSQL database is in attachment:
/web-app/backend/services/
get-parking-list-services.js
To retrieve data from a PostgreSQL database, as previously mentioned, we will use the pg
library. Since data retrieval and database connection are part of the server-side of the application, this part of the implementation will be located in the backend section of the application.
At the beginning of the implementation, you need to retrieve the module from the pg
library. The Pool module is used for connecting and working with the PostgreSQL database.
const { Pool } = require('pg');
To connect to the PostgreSQL database, you need to define the access credentials. You can do this by creating an instance of the Pool object with the appropriate configuration.
const pool = new Pool({
user: 'postgres',
password: 'postgres',
host: '127.0.0.1',
database: 'smartparking',
port: 5432,
});
To reading data from the "parkings" table, you need to create an SQL query that will be executed to fetch the required information. Through this query, you can obtain information about the parking location, parking name, and the number of available and total parking spots.
const sqlQuery = 'SELECT parking_name, longitude, latitude, total_space, free_space, city FROM parkings;'
Fixed values defined, now will be create a function that will establish a connection to the database, execute the data retrieval query, and store the results. In case of an error, the function will return an error message. You use an asynchronous function call to ensure that the web application does not wait for the database data but displays everything possible and defined in the application to the user. Once the data is retrieved, it can be displayed in the desired format.
// Asynchronous function for reading a list of parking spots from the database
async function getParkingList() {
try {
// Executing an SQL query using the base pool object
const result = await pool.query(sqlQuery);
// Returning result rows as an array
return result.rows;
} catch (error) {
// Log podatka od gresci
console.error('Error when fetching data from the database:',
error);
// Displaying the error
throw error;
}
}
At the end of the script, need to add a function to the export so that it can be used in other modules.
module.exports = {
getParkingList
}
Displaying data from the table on an interactive mapNOTE:Vue.js componenet for displaying a data from the table on an interactive map is in attachment:
/web-app/frontend/src/
ParkingMap.vue
After retrieving data about parking spots from the PostgreSQL database, we can now provide a visual representation of this data to users of the web application. We use the MapBox platform to display maps and add information about parking spots in the form of markers on the map.
Fetching and displaying maps from the MapBox platform is done using the mapbox-gl library. Inside the ParkingMap.vue script, you will find the code that displays the map to users and adds markers to the map.
The Vue script consists of three parts:
- Template: This is the design of the frontend part of the application.
- Script: This is where functions are called and prepared for display in the template part, such as calling functions to retrieve data from the PostgreSQL database.
- Style: These are the styles used for various elements, such as text size and color.
In this case, the application's template consists of loading the CSS style for the MapBox library, defining the page title, and adding a map with its dimensions specified. The map identifier is implemented within the script part of the Vue script.
<template>
<link
href="https://api.mapbox.com/mapbox-gl-js/v2.7.0/mapbox-gl.css"
rel="stylesheet">
<main class="page-style">
<header>
<h2>Parking Space Status Across the City</h2>
</header>
<div id="map" style="height: 1200px;"></div>
</main>
</template>
The function for adding the map and creating markers with parking information is created within the script part of the Vue script. This function is of asynchronous type.
async mounted() {
// Set Mapbox API token for accessing Mapbox services
mapboxgl.accessToken = 'token';
// Create a new Mapbox map and set options
this.map = new mapboxgl.Map({
// HTML element ID where the map will be displayed
container: 'map',
// You can choose different map styles
style: 'mapbox://styles/mapbox/satellite-streets-v12',
// Map center, optional
center: [18.67333799041774, 44.53745464297011],
// Initial map zoom leve
zoom: 15.5,
});
// Set an interval for automatic marker refreshing every 5 seconds
this.refreshInterval = setInterval(() => {
// Remove existing markers before calling the refreshParkingMarkers
this.parkingMarkers.forEach((marker) => marker.remove());
// Clear the list to add new markers later
this.parkingMarkers = [];
this.refreshParkingMarkers();
}, 5000); // Automatic refresh every 5 seconds
}
Markers are added and defined based on the data from the database. Each of the rows needs to be processed, and values are stored in individual variables. To iterate through each of the rows, a loop is used to save the column values from each row and create the text that will be displayed on the marker.
// Part of the refreshParkingMarkers function
// Fetches a list of parking spots from the server
const response = await this.fetchParkingList();
// Remove all existing markers and popups from the map
this.parkingMarkers.forEach((marker) => marker.remove());
// Clear the list to add new markers later
this.parkingMarkers = [];
// Iterate through parking spot data from the response
for (const row of response) {
const longitude = row['longitude'];
const latitude = row['latitude'];
const totalSpace = row['total_space'];
const freeSpace = row['free_space'];
const parkingName = row['parking_name'];
const dateUpdate = row['date_update'];
const parkingCity = row['city'];
// Create HTML text for the marker with parking spot information
const data = "<b>" + parkingName + " - "
+ parkingCity + "</b><br>(" + dateUpdate
+ ")<br><br>FREE SPACE: <b>" + freeSpace
+ "</b><br>TOTAL SPACE: <b>" + totalSpace + "</b>";
// Rest of the code inside the for loop
We have created a text that needs to be displayed on a marker and stored it in the variable markerText. To display this text at a specific location, it is necessary to create a marker and add specific values for each marker. Each marker must contain geographical coordinates to be displayed at that location. In our case, these geographical coordinates are data retrieved from the PostgreSQL database, stored in the variables latitude and longitude.
const marker = new mapboxgl.Marker()
.setLngLat([longitude, latitude])
.addTo(this.map);
After creating the marker and defining the coordinates at which it will be displayed, it is necessary to add the text we want to display on the specified marker. The text is shown in the form of an info window with HTML text that we created earlier. Additional properties inside curly braces indicate whether the info window will be closable or not. Finally, the markerText created earlier is added as the text to be displayed in the info window.
const popup = new mapboxgl.Popup({ closeButton: false,
autoPan: false,
closeOnClick: false }
).setHTML(marketText);
The marker and the text to be displayed on the marker have been created. Now, it is necessary to associate/connect this info window with the marker.
marker.setPopup(popup);
popup.addTo(this.map);
And finally, add the marker and text to the parkingMarkers list.
this.parkingMarkers.push(marker);
this.parkingMarkers.push(popup);
The for loop will iterate through each of the rows retrieved from the PostgreSQL database function. Each row will be a marker on its own, and it will be created and added to the parkingMarkers list. This list is then returned as the output of the function in which the entire script for creating markers is located. The function is named refreshParkingMarkers.
The map data is refreshed every 5 seconds. This means that every 5 seconds, data is retrieved from the database, markers are recreated, and they are displayed on the map. It is possible to set a shorter refresh interval or even remove it entirely, allowing real-time updates to be displayed.
The refresh rate is set to 5 seconds because the application runs locally on a computer; on a server, it can be significantly faster.
Reciving video from BalenaFin board to a Web ApplicationNOTE: Vue.js componenet for displaying a live camera feed in attachment:
/web-app/frontend/src/
ImageStreaming.vue
This code represents a Vue.js component for displaying a live camera feed from a Flask application. Here's a description:
<template>
<main class="page-style">
<header>
<h2>Parking - Live Camera</h2>
</header>
<div class="body-page">
<iframe width="1100" height="800" :src="iframeSource" frameborder="0" allowfullscreen></iframe>
</div>
</main>
</template>
<script>
export default {
data() {
return {
iframeSource: 'http://192.168.0.21:8000/'
};
}
}
</script>
Description:
- This Vue.js component is designed to create a Web Application for displaying live camera footage.
- The iframe element is used to embed the video stream. It has a width of 1100 pixels and a height of 800 pixels.
- The iframeSource variable contains the address where the Flask application is streaming the video. In this case, it's the IP address (192.168.0.21) of the BalenaFin board and the port (8000) where Flask is hosting the video stream.
NOTE: Make sure to replace the IP address in iframeSource with the actual IP address of your BalenaFin board.
This mobile application for smart parking represents a practical tool to facilitate the daily life of drivers and contributes to the efficiency of urban traffic. In the following sections of this chapter, we will explore all aspects of the development and implementation of this application. It is important to note that Android Studio is used for application development, along with programming languages XML (Extensible Markup Language) and Kotlin.
Get User LocationEfficiently retrieving the user's precise location is a key component of a smart parking mobile application. This functionality allows users to find the nearest available parking spot in real-time, significantly enhancing their parking experience. In this section, we will explore in detail how to implement user location retrieval using Kotlin within the Android Studio environment.
The first step towards successfully obtaining the user's location is adding the appropriate permission in the AndroidManifest.xml file. Here, we define access to the user's location:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
Additionally, if we want to specifically utilize GPS for precise positioning, we can add a permission for location:
<uses-feature android:name="android.hardware.location.gps"/>
Before requesting location updates, it's necessary to check if the application has the appropriate permissions to access the location. This can be done in the onCreate method of the activity. If the permission hasn't been granted, the application will request permission from the user to use the device's location. Here's the code snippet to achieve this check and request permission from the user:
// Check for location permission
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
// Location permission is not granted, request it from the user
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), 1)
} else {
// Location permission is already granted
// Request location updates using LocationManager
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER, 0, 0f, locationListener)
}
This code does the following:
- Checks if the location permission (Manifest.permission.ACCESS_FINE_LOCATION) has been granted using ActivityCompat.checkSelfPermission.
- If the permission has not been granted, it requests the location permission from the user using ActivityCompat.requestPermissions. The request code 1is used here.
- If the location permission is already granted, it proceeds to request location updates using the LocationManager with LocationManager.GPS_PROVIDER. The parameters 0, 0frepresent the minimum time and distance between location updates, and locationListener is the listener that will handle location updates.
Make sure to handle the result of the permission request in the onRequestPermissionsResult method to check whether the user granted or denied the permission and take appropriate action accordingly.
At the end, in order to handle the user's permission for accessing location, the onRequestPermissionsResult method has been implemented:
// Initializing the LocationManager for location management
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
// Setting up the LocationListener to monitor location status
locationListener = object : LocationListener {
// This method is called when the user's location changes
override fun onLocationChanged(location: Location) {
// Reading the latitude and longitude of the user's current location
userLatitude = location.latitude
userLongitude = location.longitude
}
// This method is called when the location provider's status changes (e.g., GPS, network)
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {
}
}
This method handles the user's response to the location access request. If it's granted, the application will start receiving location updates. If it's denied, we can provide the user with appropriate notifications about limited location access. The geographic latitude and longitude are stored in the variables userLatitude and userLongitude. These variables are used throughout the rest of the application.
Fetching parking data from a PostgreSQL databaseFirst, we defined the ParkingSpot class, which represents a data structure for information about parking lots. This class contains fields such as parking lot name, geographic coordinates, number of free and total spaces, parking price, etc.
data class ParkingSpot(
val parkingName: String, // Parking name
val latitude: Double, // Latitude of parking
val longitude: Double, // Longitude of parking
val userLatitude: Double, // The user's current latitude
val userLongitude: Double, // The user's current longitude
val totalSpace: Int, // The total number of parking space
val freeSpace: Int, // Number of available parking spaces
val parkingFee: Int, // Parking fee
val parkingCost: Double, // Parking cost
var radiusKm: Double // Search radius (kilometers)
)
Next, we set the parameters for connecting to the PostgreSQL database, including the URL, username and password. Example:
val url = "jdbc:postgresql://localhost:5432/smartparking"
val username = "postgresql_user"
val password = "postgresql_password"
- localhost is actually the local address where the PostgreSQL database is installed.
- 5432 is the port on which the base is located.
- smartparking is name of database
Default user is:
- username = "postgres"
- password = "postgres"
After setting the connection parameters, needs to be defined the SQL query to retrieve the data about parking lots from the database. This query looks for all data from the PARKINGS table and sorts it according to the number of FREE_SPACE in descending order:
val query = "SELECT * FROM PARKINGS ORDER BY FREE_SPACE DESC;"
In order to avoid blocking the main loop, an Executor was created to execute the SQL query on the second loop. This is where the database is connected and query results are processed. (The code is commented, the comments show what each line of code does)
//List to save information about parking places
val parkingList = mutableListOf<ParkingSpot>()
//Initializing the Executor to execute the SQL query on the second loop
val executor = Executors.newSingleThreadExecutor()
executor.execute {
try {
//Connecting to a PostgreSQL database
val connection = DriverManager.getConnection(
url,
username,
password)
val statement = connection.createStatement()
val resultSet = statement.executeQuery(query)
//If there are results from the database
runOnUiThread {
if (resultSet != null) {
while (resultSet.next()) {
// Retrieving parking data from query results
val latitude = resultSet.getDouble("latitude")
val longitude = resultSet.getDouble("longitude")
val freeSpace = resultSet.getInt("free_space")
val totalSpace = resultSet.getInt("total_space")
val parkingName = resultSet.getString("parking_name")
val parkingFee = resultSet.getInt("parking_fee")
val parkingCost = resultSet.getDouble("parking_cost")
// Creation of ParkingSpot object with
// with the obtained data and adding it to the list
val parkingSpot = ParkingSpot(
parkingName,
latitude,
longitude,
userLatitude,
userLongitude,
totalSpace,
freeSpace,
parkingFee,
parkingCost
)
parkingList.add(parkingSpot)
}
resultSet.close()
}
}
}
//Exception handling if a problem occurs when executing an SQL query
catch (e: Exception) {
e.printStackTrace()
}
This demonstrates how data is received from a PostgreSQL database, processed, and prepared for display. This piece of code executes an SQL query in a secondary loop to prevent blocking the main loop and ensure application responsiveness.
Now, all the data is retrieved from the PARKINGS table. Not all of this data will be displayed to the user; it is necessary to filter it based on the user's location. The retrieved data, as previously mentioned, is sorted by the free_space column in descending order. This means that parking lots with the largest number of free parking spaces are located at the beginning of the list.
Allocation of parking spaces within a radius of 5 kmWhen defining the ParkingSpot class, we added a field to store the radius in kilometers. This data will be filled in after we obtain information from the calculateRadius function. The calculateRadius function is responsible for determining the distance between two map coordinates.
To calculate the distance between two points, we can use the Haversine formula, which is used to calculate distances between points on a sphere.
Applying this formula to geographic coordinates is possible if we convert latitude and longitude into radians. The calculateRadius function should look like this:
private fun calculateRadius( lat1: Double,
lon1: Double,
lat2: Double,
lon2: Double):
Double {
//Earth's radius in kilometers
val radiusOfEarth = 6371.0
//Convert latitude and longitude to radians
val lat1Rad = Math.toRadians(lat1)
val lon1Rad = Math.toRadians(lon1)
val lat2Rad = Math.toRadians(lat2)
val lon2Rad = Math.toRadians(lon2)
//Difference in latitudes and longitudes
val dLat = lat2Rad - lat1Rad
val dLon = lon2Rad - lon1Rad
//Formula for distance
val a = sin(dLat / 2).pow(2) + cos(lat1Rad)
* cos(lat2Rad) * sin(dLon / 2).pow(2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
val radius = radiusOfEarth * c // Distance in kilometers
return radius
}
This function will perform the calculation for two locations. Since we have the ParkingSpot class that stores a larger number of parking spots retrieved from the database, it is necessary to create another function that iterates through ParkingSpot objects and adds information about the radius.
private fun calculateDistances(parkingList: List<ParkingSpot>) {
for (parkingSpot in parkingList) {
//Calculate distance
val radius = calculateRadius( userLatitude,
userLongitude,
parkingSpot.latitude,
parkingSpot.longitude )
parkingSpot.radiusKm = radius
}
}
So, the calculateDistances function will iterate through each object from the ParkingSpot class and calculate the distance between the user's current location and the parking location. The value obtained at the output of the calculateRadius function will be stored as the radiusKm value for each object. With this, we have completed the ParkingSpot class, where we now have all the necessary information about individual parking spots.
To filter out parking spots that are located at a distance of 5 kilometers from the user, we will use the built-in filter function.
calculateDistances(parkingList)
//Desired radius from which parking spots will be extracted
val radiusKmConst = 5.0
//Now calculate distances and filter the data
val filteredList = parkingList.filter { it.radiusKm < radiusKmConst }
filteredList represents a list of parking spots located in this case within a 5-kilometer radius around the user.
Displaying the data and extracting parking spots with available parking spacesAfter obtaining the final list of parking spots located within a 5-kilometer radius around the user, these data need to be presented to the user in the application. So far, we have worked on the back-end implementation of the application, which is written in the Kotlin programming language. The front-end part of the application is written in the XML programming language.
We are using a RecyclerView from XML in Android Studio, which allows us to display a list of data. This can be an array of numbers or, in our case, data from the database formatted for display in the application. In figure, the design for one of the parking spots is shown. Each parking spot will have the same format with different data.
The data displayed is retrieved from the database in the following way:
- The parking spot name is retrieved from the parking_name column. This was previously retrieved and stored in the parkingName field within the parkingSpot object. We display the text using the following command:
holder.parkingSpaceName.text = parkingSpot.parkingName
- The number of available parking spaces is retrieved from the free_space column. If the number of available parking spaces is 0, the text and number will be colored in red, and if there are available spaces, they will be displayed in green.
if (parkingSpot.freeSpace > 0) {
//Changing the color of the text "FREE SPACE" and the number
holder.textViewFreeSpace.setTextColor(green)
holder.textViewFreeText.setTextColor(green)
} else {
//Changing the color of the text "FREE SPACE" and the number
holder.textViewFreeSpace.setTextColor(red)
holder.textViewFreeText.setTextColor(red)
}
- The total number of parking spaces is retrieved from the total_space column.
holder.textViewTotalSpace.text = parkingSpot.totalSpace.toString()
- The payment status is retrieved from the parking_fee and parking_cost columns as follows:
- If parking_fee = 1, it means it's a paid parking. In this case, a message will be presented indicating that the parking is paid, along with the parking cost, which is retrieved from the parking_cost column. For example: "Parking cost: 1BAM/h."(BAM - Bosnian Convertible Mark)
- If parking_fee = 0, it means the parking is free. In this case, a message will be displayed that the parking is free, and the parking_cost column will not be considered.
if (parkingSpot.parkingFee === 1) {
holder.textViewParkingFee.text =
"Parking cost: " +
parkingSpot.parkingCost +
" BAM/h"
holder.textViewParkingFee.setTextColor(gray)
} else {
holder.textViewParkingFee.text = "Free Parking"
holder.textViewParkingFee.setTextColor(green)
}
- The button for opening the map will open a map for the user, with the exact location of the parking spot marked. The location data is retrieved from the database, specifically from the parking_latitude and parking_longitude columns.
- The data displayed on the button about the parking distance is obtained using the calculateDistance function. This function takes the parking location and the user's current location as input, and the function returns the parking distance in kilometers and minutes. (Details about the function can be found in the ParkingListView.kt appendix.)
If we were to leave the display design like this without additional data analysis, the user could choose any parking spot and request directions to it. The application should not guide the user to a parking spot that has no available spaces, so it's necessary to filter out the parking spots that have no available spaces and disable the selection of those parking spots. We can disable the selection of such parking spots and blur them in the display with the following code:
holder.mapButton.isEnabled = false //Disable button
holder.cardViewParking.alpha = 0.4f //The background opacity is set to 40%.
The difference between a parking spot that has available parking spaces and one that doesn't can be seen in the image:
In this part, we will explore in detail how to implement MapBox maps into the mobile application for smart parking. MapBox maps allow users to visually track their location and destination on the map, which is crucial for navigating to desired parking spots. As mentioned earlier, distance data is calculated using the calculateDistance function. The function is also based on the MapBox API, which returns data on the distance by car from the user in minutes and kilometers.
It has been explained that, upon clicking the button, a map opens with the parking location displayed. The parking location will be shown using MapBox maps. This implementation can be done through several steps, with the primary one being adding dependencies in the application-level build.gradle file.
implementation ("com.mapbox.maps:android:10.15.1"){
exclude (group = "group_name", module = "module_name")
}
implementation ("com.mapbox.mapboxsdk:mapbox-sdk-services:6.13.0")
Next, the map is initialized, and data is retrieved from the previous activity.
var mapView: MapView? = null
val parkingLatitude = intent.getDoubleExtra("latitude", 0.0)
val parkingLongitude = intent.getDoubleExtra("longitude", 0.0)
val parkingName = intent.getStringExtra("parkingName")
textViewNavigation.text = parkingName
Additionally, text that will be displayed at the bottom of the window is added. After adding the text, the camera position on the map is configured to set the desired zoom level and center the map on the parking coordinates.
val cameraPosition = CameraOptions.Builder()
.zoom(17.0)
.center(Point.fromLngLat(parkingLongitude, parkingLatitude))
.build()
We initialize the MapView with the appropriate ID (R.id.map_view) and load the map style, in this case, SATELLITE_STREETS, which combines satellite view with streets.
var mapView = findViewById<MapView>(R.id.map_view)
mapView?.getMapboxMap()?.loadStyleUri(Style.SATELLITE_STREETS)
Set the camera on the created map.
mapView.getMapboxMap().setCamera(cameraPosition)
Finally, we prepare and add a marker to the map. First, we obtain a CircleAnnotationManager through the annotations API, then configure a CircleAnnotationOptions object with specific parameters such as position, size, color, and stroke width. Finally, we use the create method to add the circle to the map.
val annotationApi = mapView?.annotations
val circleAnnotationManager =
annotationApi?.createCircleAnnotationManager(mapView)
val circleAnnotationOptions: CircleAnnotationOptions =
CircleAnnotationOptions()
.withPoint(Point.fromLngLat(parkingLongitude,
parkingLatitude))
.withCircleRadius(8.0)
.withCircleColor("#ee4e8b")
.withCircleStrokeWidth(2.0)
.withCircleStrokeColor("#ffffff")
circleAnnotationManager?.create(circleAnnotationOptions)
The final look, including the button described in the next section, is shown in the image.
This implementation of MapBox maps enhances the mobile application by allowing users to visually track their location and destination on the map, giving users the option to choose different parking spots based on their needs with a visual overview of the parking spot's location.
Integration with Google navigationIntegration with Google navigation is a key aspect of our smart parking mobile application. It allows users to quickly and easily find and navigate to their desired parking spots. In the user interface, we use a button to initiate Google Navigation. This button on the map display is green and labeled "DRIVE." To the left of the button is the name of the parking spot that the user previously selected. The user is provided with the option to start navigation to the chosen parking spot with a simple click on this button. The implementation of this call in Kotlin is done through the code below:
var buttonGoogleMaps: Button = findViewById(R.id.button_google_maps)
buttonGoogleMaps.setOnClickListener {
val uri = "geo:$\dollar$parkingLatitude,
$\dollar$parkingLongitude=
$\dollar$parkingLatitude,
$\dollar$parkingLongitude($\dollar$parkingName)"
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
intent.setPackage("com.google.android.apps.maps")
if (intent.resolveActivity(packageManager) != null) {
startActivity(intent)
} }
When a user clicks on the button to start Google navigation, the application generates an appropriate URI that contains the geographical coordinates (latitude and longitude) of the parking spot, as well as the name of the parking spot. This URI is used to initiate Google navigation through the corresponding Intent. This integration makes it easier for users to quickly find and arrive at their chosen parking spot using the popular Google navigation, enhancing the overall user experience and making the application more functional and useful. The navigation process within the app leading to the final Google navigation is shown step by step in the image.
The first video is more like a working version. In the second video, applications are shown during rapid changes in the condition of parking lots.
Comments