B4J Code Snippet [PyBridge] Using folium (OpenStreetMap) in a webview

As requested in another thread here is how to setup a B4XPages app to use PyBridge and Folium in a webview.

Download latest beta version of B4J with PyBridge.

Make sure PyBridge is selected in the Libraries Manager Tab

Add folium to Python (pip install folium in a command prompt window for ypur Python, or use the Open local Python shell line below if python not installed)

B4X Code (B4XMainPage)
B4X:
#CustomBuildAction: after packager, %WINDIR%\System32\robocopy.exe, Python temp\build\bin\python /E /XD __pycache__ Doc pip setuptools tests

'Export as zip: ide://run?File=%B4X%\Zipper.jar&Args=Project.zip
'Create a local Python runtime:   ide://run?File=%WINDIR%\System32\Robocopy.exe&args=%B4X%\libraries\Python&args=Python&args=/E
'Open local Python shell: ide://run?File=%PROJECT%\Objects\Python\WinPython+Command+Prompt.exe
'Open global Python shell - make sure to set the path under Tools - Configure Paths. Do not update the internal package.
'ide://run?File=%B4J_PYTHON%\..\WinPython+Command+Prompt.exe

Sub Class_Globals
    Private Root As B4XView
    Private xui As XUI
    Public Py As PyBridge
    Private WebView1 As WebView
    Private Button1 As Button
End Sub

Public Sub Initialize
   
End Sub

'This event will be called once, before the page becomes visible.
Private Sub B4XPage_Created (Root1 As B4XView)
    Root = Root1
    Root.LoadLayout("MainPage")
    Py.Initialize(Me, "Py")
    Dim opt As PyOptions = Py.CreateOptions("d:/python.3.12.8/python.exe") ' change to where your python is if required
    Py.Start(opt)
    Wait For Py_Connected (Success As Boolean)
    If Success = False Then
        LogError("Failed to start Python process.")
        Return
    End If
End Sub

Sub Button1_Click
    folium_test
End Sub

Private Sub B4XPage_Background
    Py.KillProcess
End Sub

Private Sub Py_Disconnected
    Log("PyBridge disconnected")
End Sub

Sub folium_test
    ' I read from dir assets but the code could be a string in-line.
    ' change the Array to contain a string of where you want the map to open eg "London, UK" from "Toronto, CA"
    wait for ((Py.RunCode("start",Array("Toronto, CA"),File.ReadString(File.DirAssets,"folium_test.py")).Fetch)) Complete (res As PyWrapper)
    ' load the produced html file into a webview (it is fully functional web page and will open in  browser too if you want)
    WebView1.LoadHtml(File.ReadString("../objects",res.value))
End Sub

The Python code (either in-line or in a file)
Note in the following code the line with 'an email address' - the lookup requires an email address
B4X:
import folium
import requests
def get_lat_lon(place_name):
    url = f"https://nominatim.openstreetmap.org/search?q={place_name}&format=json"
    headers = {
        'User-Agent': 'AddressLookup/1.0 (an email address)'
    }
    response = requests.get(url, headers=headers)
    if response.status_code == 200:
        try:
            data = response.json()
            if data:
                lat = data[0]['lat']
                lon = data[0]['lon']
                return lat, lon
            else:
                print("No data found for the specified place name.")
                return None
        except ValueError:
            print("Invalid JSON response.")
            return None
    else:
        print(f"Error: {response.status_code}")
        return
def start(place):
    lat,lon = get_lat_lon(place)
    # Create a map centered around Lat/Lon
    map_london = folium.Map(location=[lat,lon], zoom_start=12)
    # Add a marker for Home
    folium.Marker(
        location=[lat,lon],
        popup="Marker",
        icon=folium.Icon(icon="Marker")).add_to(map_london)
   
    # Save the map to an HTML file -- change the name to what you want, it will be saved in /Objects folder
    map_london.save("london_map.html")
    # return the name of the file to B4J
    return "london_map.html"
 
Last edited:

Daestrum

Expert
Licensed User
Longtime User
Is there a way to save the map without resetting its location and zoom level?
Sorry only just saw this question. I will have a look, its possible (I think) to get the centre coord of the view and the zoom level, so the next call to save the map can use that.

I will have a play and see what comes up.

I have had thoughts on the ablity (or lack of) to remove markers/lines etc from the map. As in post #19 I found you dont need to save the map, you can actually send the html back to B4J. So in theory you could edit the html to remove markers etc.
 

Ralph Parkhurst

Member
Licensed User
Longtime User
... get the centre coord of the view and the zoom level ...

I do hope you can find this data Daestrum. 🙂

From my online research, Folium is a terrific visualiser, however returning useful data out of it is extremely difficult. As an example, I wanted to return the lat/lon coordinates of a polygon I had drawn. The only way I could find was to use Folium's 'export' function which places the geojson data into the clipboard, then I read it in B4J using fx.Clipboard.GetString. Its clumsy, but it works.

For what it's worth, here's the way I store polygons and markers persistently and apply ID codes to facilitate removal or update. It is all based upon Daestrum's original tutorial code...

folium_test3a.py:
import folium
from folium.plugins import MarkerCluster, Draw, MousePosition
import json
import os

# File paths for persistence
MARKERS_FILE = "markers.json"
POLYGONS_FILE = "polygons.json"
MAP_FILE = "map.html"

# Global variable to store the map instance
#map_EWS = None
global map_EWS


# Load markers and polygons
def load_markers():
    if os.path.exists(MARKERS_FILE):
        with open(MARKERS_FILE, "r") as f:
            try:
                return json.load(f)
            except json.JSONDecodeError:
                return {}
    return {}


def save_markers():
    with open(MARKERS_FILE, "w") as f:
        json.dump(markers, f, indent=4)


def load_polygons():
    if os.path.exists(POLYGONS_FILE):
        with open(POLYGONS_FILE, "r") as f:
            try:
                return json.load(f)
            except json.JSONDecodeError:
                return {}
    return {}


def save_polygons():
    with open(POLYGONS_FILE, "w") as f:
        json.dump(polygons, f, indent=4)

markers = load_markers()
polygons = load_polygons()


# Start map (Called once at entry point)
def start_map(lat, lon, zoom):
    global map_EWS
    global current_lat, current_lon, current_zoom
    current_lat, current_lon, current_zoom = lat, lon, zoom
    print(f"📍 Starting new map at: lat={current_lat}, lon={current_lon}, zoom={current_zoom}")
    regenerate_map()


# Regenerate map
def regenerate_map():
    global map_EWS
  
    map_EWS = folium.Map(location=[current_lat,current_lon], zoom_start=current_zoom,tiles="OpenStreetMap")
    marker_cluster = MarkerCluster().add_to(map_EWS)

    formatter = "function(num) {return L.Util.formatNum(num, 4) + ' ° ';};"
    MousePosition(
        position="bottomleft", separator=" | ", empty_string="NaN",
        lng_first=False, num_digits=22, prefix="Coordinates:",
        lat_formatter=formatter, lng_formatter=formatter
    ).add_to(map_EWS)

    for marker_id, data in markers.items():
        lat, lon, popup_text = data["lat"], data["lon"], data["popup_text"]
        folium.Marker(
            location=[lat, lon],
            popup=f"{popup_text} (ID: {marker_id})",
            tooltip=f"Marker {marker_id}"
        ).add_to(marker_cluster)

    for polygon_id, data in polygons.items():
        coordinates, label, color = data["coordinates"], data["label"], data["color"]
        folium.Polygon(
            locations=coordinates,
            color=color,
            weight=3,
            fill=True,
            fill_opacity=0.3,
            fill_color=color,
            popup=f"{label} (ID: {polygon_id})",
            tooltip=f"Polygon {polygon_id}"
        ).add_to(map_EWS)

    draw_options = {
        "polyline": False,
        "polygon": {
            "shapeOptions": {
                "color": "red",
                "weight": 3,
                "fillColor": "blue",
                "fillOpacity": 0.3
            }
        },
        "rectangle": False,
        "circle": False,
        "marker": False,
        "circlemarker": False
    }

    Draw(export=True, position='topleft', filename='data.geojson', draw_options=draw_options, edit_options={'poly': {'allowIntersection': False}}).add_to(map_EWS) 
    save_map()


# Save map
def save_map():
    global map_EWS, current_lat, current_lon, current_zoom
    if map_EWS:
        map_EWS.save(MAP_FILE)
        print(f"✅ Map saved as {MAP_FILE}")
    else:
        print("❌ Error: map_EWS is not initialized.")
    return MAP_FILE


# Marker functions
def add_marker(marker_id, lat, lon, popup_text):
    if marker_id in markers:
        print("Error: Marker ID already exists. Marker not added.")
        return
    markers[marker_id] = {"lat": lat, "lon": lon, "popup_text": popup_text}
    save_markers()
    regenerate_map()
    return save_map()


def update_marker(marker_id, new_lat, new_lon, new_popup_text):
    if marker_id not in markers:
        print("Error: Marker not found. Position not updated.")
        return
    markers[marker_id] = {"lat": new_lat, "lon": new_lon, "popup_text": new_popup_text}
    save_markers()
    regenerate_map()
    return save_map()


def remove_marker(marker_id):
    if marker_id not in markers:
        print("Error: Marker not found. Marker not removed.")
        return
    del markers[marker_id]
    save_markers()
    regenerate_map()
    return save_map()

# Polygon functions
def add_polygon(polygon_id, coordinates, label="Polygon", border_color="red"):
    valid_colors = {"blue", "red", "black", "green"}
    if polygon_id in polygons:
        print("Error: Polygon ID already exists. Polygon not added.")
        return
    if border_color not in valid_colors:
        border_color = "red"
    polygons[polygon_id] = {"coordinates": coordinates, "label": label, "color": border_color}
    save_polygons()
    regenerate_map()
    return save_map()


def update_polygon(polygon_id, new_coordinates, new_label="Polygon", new_border_color="red"):
    valid_colors = {"blue", "red", "black", "green"}
    if polygon_id not in polygons:
        print("Error: Polygon not found. Polygon not updated.")
        return
    polygons[polygon_id]= {"coordinates": new_coordinates, "label": new_label, "color": new_border_color}
    save_polygons()
    regenerate_map()
    return save_map()


def remove_polygon(polygon_id):
    if polygon_id not in polygons:
        print("Error: Polygon not found. Polygon not removed.")
        return
    del polygons[polygon_id]
    save_polygons()
    regenerate_map()
    return save_map()

This is very new to me

Well, it's also brand new to me Javiers 🙂
 
Last edited:

javiers

Active Member
Licensed User
Longtime User
It seems that there is some good news regarding the jGoogle Maps 2.01 issue. My programs use it and I was looking for other alternatives. But if this materializes, I will continue to work on that line of work, since it seems that it will not require any changes to the code already written!

jGoogle Maps 2.01 issue...
 

Ralph Parkhurst

Member
Licensed User
Longtime User
It seems that there is some good news regarding the jGoogle Maps 2.01 issue. My programs use it and I was looking for other alternatives. But if this materializes, I will continue to work on that line of work, since it seems that it will not require any changes to the code already written!

jGoogle Maps 2.01 issue...
Thanks for the detail - Star-Dust is worth his weight in gold if this is a drop-in replacement library :)
 
Top