Tutorial 05 – MyTransportApp: Bus Stop Screen Front Layer with Kivy Garden Mapview

By the end of this tutorial, you will be able to build a screen like this.

Step 1: As mentioned in the last tutorial, we are going to use data from LTA Datamall Section 2.4 Bus Stops

https://www.mytransport.sg/content/mytransport/home/dataMall/dynamic-data.html

https://www.mytransport.sg/content/dam/datamall/datasets/LTA_DataMall_API_User_Guide.pdf

Step 2: Make sure you have sqlite3 application inside your “MyTransportApp” project. If not, you can download it from the link below

https://www.sqlite.org/download.html

Step 3: Open the command prompt window from the folder which you save “MyTransportApp” project. From here, we are going to create our database, “myapp.db”.

Step 4: Type the following command:

sqlite3 myapp.db

Step 5: Now close the cmd window. We go back to your “MyTransportApp” and ready to write some codes. Firstly, we create a python file known as create_db.py. We want to create a table known as “busstops” inside our “myapp.db”.

import sqlite3

connection = sqlite3.connect('myapp.db')

cursor = connection.cursor()

cursor.execute("""
    CREATE TABLE IF NOT EXISTS busstops (
    BusStopCode TEXT NOT NULL,
    RoadName TEXT NOT NULL,
    Description TEXT NOT NULL,
    Latitude REAL NOT NULL,
    Longitude REAL NOT NULL
)
""")

connection.commit()

Step 6: Then we will create another python file known as populate_db.py. Here we are going to populate and save the bus stops data that we query from the datamall url into the “busstops” table inside the “myapp.db”

import sqlite3
import requests, json
from apikey_ import lta_account_key

#Authentication parameters
headers = {'AccountKey' : lta_account_key, 'accept' : 'application/json'} #this is by default
#API parameters
uri = 'http://datamall2.mytransport.sg/' #Resource URL

connection = sqlite3.connect("myapp.db")
cursor = connection.cursor()

bus_stops_path = "ltaodataservice/BusStops"
bus_stops_skip = '?$skip='
for i in range(0, 11):
    if i == 0:
        bus_stops_url = uri + bus_stops_path
        bus_stops = requests.get(bus_stops_url, headers=headers)
        bus_stops_response = json.loads(bus_stops.content)
        for bus_stop in bus_stops_response['value']:
            try:
                cursor.execute("""INSERT INTO busstops (BusStopCode, RoadName, Description, Latitude, Longitude) VALUES (?, ?, ?, ?, ?);""", (bus_stop['BusStopCode'], bus_stop['RoadName'], bus_stop['Description'], bus_stop['Latitude'], bus_stop['Longitude']))
                print(f"Added a new bus stop {bus_stop['BusStopCode']} {bus_stop['Description']}")
            except Exception as e:
                print(bus_stop['BusStopCode'])
                print(e)

    else:
        bus_stops_url = uri + bus_stops_path + bus_stops_skip + str(i*500)
        bus_stops = requests.get(bus_stops_url, headers=headers)
        bus_stops_response = json.loads(bus_stops.content)
        for bus_stop in bus_stops_response['value']:
            try:
                print(f"Added a new bus stop {bus_stop['BusStopCode']} {bus_stop['Description']}")
                cursor.execute("""INSERT INTO busstops (BusStopCode, RoadName, Description, Latitude, Longitude) VALUES (?, ?, ?, ?, ?);""", (bus_stop['BusStopCode'], bus_stop['RoadName'], bus_stop['Description'], bus_stop['Latitude'], bus_stop['Longitude']))
            except Exception as e:
                print(bus_stop['BusStopCode'])
                print(e)

connection.commit()

Step 7: To check whether your bus stops data have been populated and saved correctly, we can use SELECT sql command. However, for the sake of easier understanding for beginner, we will use DB Browser (SQ Lite) which is available at:

https://sqlitebrowser.org/

Step 8: When the bus stop data are stored, we can go back to our main app and ready to query the data from the database. First, to use SQL command in Python script, you need to import sqlite3.

import sqlite3

Step 9: In main.py, under MainApp(MDApp):, add the following:

connection = None
cursor = None
total_bus_stops_response  = []

Step 10: In main.py, under MainApp(MDApp), within the function on_start():

# Connect to database
self.connection = sqlite3.connect("myapp.db")
self.cursor = self.connection.cursor()

# Get the total bus stops
self.busstopsquery()

Step 11: We will create a new function under MainApp(MDApp) for query all the bus stops data from our database’s table “busstops”.

def busstopsquery(self):
    self.cursor.execute("""SELECT * FROM busstops ;""")
    self.total_bus_stops_response = self.cursor.fetchall()
    self.connection.commit()

Step 12: Moving next, we want to create the bus stop mapview in the front layer of back drop. In busstopbackdroplayout.py, add the following.

#:include busstop_folder/busstopmapview.kv

Step 13: In the change_screen() function, under if screen_name == “bus_stop_screen”:

try:
    # Remove busstopmapview widget
    for w in bus_stop_backdropfrontlayer.walk():
        if w.__class__ == BusStopMapView:
            print("remove busstopmapview widget")
            bus_stop_backdropfrontlayer.remove_widget(w)
        else:
            continue
except:
    print("Something is wrong")
    pass

self.busstopmapview = BusStopMapView()
bus_stop_backdropfrontlayer.add_widget(self.busstopmapview)

self.busstopmapview.center_on(self.current_lat, self.current_lon)

Step 14: Under class MainApp(MDApp):

busstopmapview = ObjectProperty(None)

Step 15: In the busstop_folder, create busstopmapview.py

from kivy_garden.mapview import MapView
from kivy.clock import Clock
from kivy.app import App
from busstop_folder.busstopmarker import BusStopMarker

class BusStopMapView(MapView):
    getting_busstop_timer = None
    bus_stop_code = []

    def start_getting_busstop_in_fov(self):
        try:
            self.getting_busstop_timer.cancel()
        except:
            pass
        self.getting_busstop_timer = Clock.schedule_once(self.get_busstop_in_fov, 1)

    def get_busstop_in_fov(self, *args):
        min_lat, min_lon, max_lat, max_lon = self.get_bbox()
        total_bus_stops = App.get_running_app().total_bus_stops_response

        self.bus_stop_code = []
        for busstop in total_bus_stops:
            if busstop[4] > min_lon and busstop[4] < max_lon and busstop[3] > min_lat and busstop[3] < max_lat:
                code = busstop[0]
                if code in self.bus_stop_code:
                    print("code already exist")
                    continue
                else:
                    self.add_bus_stop(busstop)

    def add_bus_stop(self, busstop):
        # Create the MarketMarker
        lat, lon = busstop[3], busstop[4]
        marker = BusStopMarker(lat=lat, lon=lon)
        marker.bus_stop_data = busstop
        # Add the BusStopMarker to the map
        self.add_widget(marker)

        # Send the nearby bus stop data to main
        nearbybusstop = busstop
        App.get_running_app().update_nearby_busstop(nearbybusstop)

        # Keep track of the bus stop code
        code = busstop[0]
        self.bus_stop_code.append(code)

Step 16: In the busstop_folder, create busstopmapview.kv

#:import MapView kivy_garden.mapview.MapView
#:import GpsBlinker gpsblinker.GpsBlinker

<BusStopMapView>:
    zoom: 16
    double_tap_zoom: True
    on_zoom:
        self.zoom = 16 if self.zoom < 16 else self.zoom # set minimum zoom = 16
    on_lat:
        self.start_getting_busstop_in_fov() # on getting latitude
    on_lon:
        self.start_getting_busstop_in_fov() # on getting longitude
    GpsBlinker:
        lat: root.lat
        lon: root.lon
        id: busstopblinker

Step 17: In the busstop_folder, create busstopmarker.py

from kivy_garden.mapview import MapMarkerPopup
from busstop_folder.busstoppopupmenu import BusStopPopupMenu

class BusStopMarker(MapMarkerPopup):
    source = "icons/bus-stop.png"
    bus_stop_data = []

    def on_release(self):
        menu = BusStopPopupMenu(self.bus_stop_data)
        menu.size_hint = [.8, .6]
        menu.open()

Step 18: In the busstop_folder, create busstoppopupmenu.py

from kivymd.uix.dialog import BusStopMDDialog

class BusStopPopupMenu(BusStopMDDialog):
    def __init__(self, bus_stop_data):
        super().__init__()

        # Set all of the fields of market data
        headers = "BusStopCode,RoadName,Description,Latitude,Longitude"
        headers = headers.split(',')

        for i in range(len(headers)):
            attribute_name = headers[i]
            attribute_value = str(bus_stop_data[i])
            setattr(self, attribute_name, attribute_value)

Step 19: In the kivymd.uix.dialog,create class BusStopMDDialog. If you are using my version of kivymd from my Github page, you already have this in your kivymd.

class BusStopMDDialog(BaseDialog):
    BusStopCode = StringProperty("Missing data")
    RoadName = StringProperty("Missing data")
    Description = StringProperty("Missing data")
    Latitude = StringProperty("Missing data")
    Longitude = StringProperty("Missing data")
    background = StringProperty(f'{images_path}ios_bg_mod.png')

Step 20: In the kivymd.uix.dialog, under the builder.load_string(), we will have this. If you are using the kivymd folder from my Github repository, you already have this code.

<BusStopMDDialog>
    title: "Bus Stop Details"
    BoxLayout:
        orientation: 'vertical'
        padding: dp(15)
        spacing: dp(10)
    
        MDLabel:
            id: title
            text: root.title
            font_style: 'H6'
            halign: 'left' if not root.device_ios else 'center'
            valign: 'top'
            size_hint_y: None
            text_size: self.width, None
            height: self.texture_size[1]
    
        ScrollView:
            id: scroll
            size_hint_y: None
            height:
                root.height - (title.height + dp(48)\
                + sep.height)
    
            canvas:
                Rectangle:
                    pos: self.pos
                    size: self.size
                    #source: f'{images_path}dialog_in_fade.png'
                    source: f'{images_path}transparent.png'
    
            MDList:
                id: list_layout
                size_hint_y: None
                height: self.minimum_height
                spacing: dp(15)
                canvas.before:
                    Rectangle:
                        pos: self.pos
                        size: self.size
                    Color:
                        rgba: [1,0,0,.5]   
                ThinBox:
                    ThinLabel:
                        text: "BusStopCode: "
                    ThinLabel:
                        text: root.BusStopCode
                ThinBox:
                    ThinLabel:
                        text: "RoadName: "
                    ThinLabel:
                        text: root.RoadName
                ThinBox:
                    ThinLabel:
                        text: "Description: "
                    ThinLabel:
                        text: root.Description
                ThinBox:
                    ThinLabel:
                        text: "Latitude: "
                    ThinLabel:
                        text: root.Latitude
                ThinBox:
                    ThinLabel:
                        text: "Longitude: "
                    ThinLabel:
                        text: root.Longitude

        MDSeparator:
            id: sep

Step 21: Under main.py, of course we want to import BusStopMapView class.

from busstop_folder.busstopmapview import BusStopMapView

Setp 22: In the busstopmapview.py, we call a function at the main.py known as update_nearby_busstop(). The reason of doing this is to send the nearby bus stops data to the main.py, so that we can use it to create our bus stop list widget at the backlayer of the backdrop in the next video.

def update_nearby_busstop(self, *args):
    # pass here from busstopmapview.py
    self.nearby_bus_stops.append(args[0])

Step 23: Under class MainApp(MDApp):

nearby_bus_stops = []
nearby_bus_stops_backup = []

Step 24: Create busstopgpshelper.py under busstop_folder

from kivy.app import App
from kivy.utils import platform
from kivymd.uix.dialog import MDDialog
from kivy.clock import Clock
from kivy.app import App

class BusStopGpsHelper():
    has_centered_map = False
    dialog = None
    def run(self):
        # Get a reference to GpsBlinker, then call blink()
        bus_stop_gps_blinker = App.get_running_app().busstopmapview.ids.busstopblinker

        # Start blinking the GpsBlinker
        bus_stop_gps_blinker.blink()
        
        # Request permissions on Android
        if platform == 'android':
            from android.permissions import Permission, request_permissions
            def callback(permission, results):
                if all([res for res in results]):
                    print("Got all permissions")
                    from plyer import gps
                    gps.configure(on_location=self.update_blinker_position, on_status=self.on_auth_status)
                    gps.start(minTime=1000, minDistance=1)
                else:
                    print("Did not get all permissions")

            request_permissions([Permission.ACCESS_COARSE_LOCATION,
                                 Permission.ACCESS_FINE_LOCATION], callback)

        # Configure GPS
        if platform == 'ios':
            from plyer import gps
            gps.configure(on_location=self.update_blinker_position, on_status=self.on_auth_status)
            # on_location = function to call when receiving new location
            # on_status = function to call when a status message received
            gps.start(minTime=1000, minDistance=0)

    def update_blinker_position(self, *args, **kwargs):
        my_lat = kwargs['lat']
        my_lon = kwargs['lon']
        print("GPS POSITION", my_lat, my_lon)
        # Update GpsBlinker position
        bus_stop_gps_blinker = App.get_running_app().busstopmapview.ids.busstopblinker
        bus_stop_gps_blinker.lat = my_lat
        bus_stop_gps_blinker.lon = my_lon

        # Center map on gps
        if not self.has_centered_map:
            bus_stop_map = App.get_running_app().busstopmapview
            bus_stop_map.center_on(my_lat, my_lon)
            self.has_centered_map = True

        App.get_running_app().current_lat = my_lat
        App.get_running_app().current_lon = my_lon

    def on_auth_status(self, general_status, status_message):
        if general_status == 'provider-enabled':
            pass
        else:
            print("Open gps access popup")
            try:
                self.open_gps_access_popup()
            except:
                print("error")
                pass

    def open_gps_access_popup(self):
        if not self.dialog:
            self.dialog = "STOP"
            Clock.schedule_once(self.run_dialog, 2)

    def run_dialog(self, *args):
        self.dialog = MDDialog(title="GPS Error", text="You need to enable GPS access for the app to function properly", size_hint=(0.5, 0.5))
        self.dialog.pos_hint = {'center_x': .5, 'center_y': .5}
        self.dialog.open()
        self.dialog = None

Step 25: Under main.py, if screen_name == “bus_stop_screen”:, add the following

from busstop_folder.busstopgpshelper import BusStopGpsHelper
BusStopGpsHelper().run()

End of Tutorial 05.

Youtube video: