importScripts("https://cdn.jsdelivr.net/pyodide/v0.23.0/full/pyodide.js"); function sendPatch(patch, buffers, msg_id) { self.postMessage({ type: 'patch', patch: patch, buffers: buffers }) } async function startApplication() { console.log("Loading pyodide!"); self.postMessage({type: 'status', msg: 'Loading pyodide'}) self.pyodide = await loadPyodide(); self.pyodide.globals.set("sendPatch", sendPatch); console.log("Loaded!"); await self.pyodide.loadPackage("micropip"); const env_spec = ['markdown-it-py<3', 'https://cdn.holoviz.org/panel/1.1.0/dist/wheels/bokeh-3.1.1-py3-none-any.whl', 'https://cdn.holoviz.org/panel/1.1.0/dist/wheels/panel-1.1.0-py3-none-any.whl', 'pyodide-http==0.2.1', 'holoviews>=1.15.4', 'holoviews', 'numpy', 'requests', 'pandas', 'pyproj', 'https://cdn.jsdelivr.net/gh/ivandorte/Rome-in-transit@main/wheels/gtfs_realtime_bindings-1.0.0-py3-none-any.whl', 'https://cdn.jsdelivr.net/gh/ivandorte/Rome-in-transit@main/wheels/protobuf-4.23.3-py3-none-any.whl', 'https://cdn.jsdelivr.net/gh/ivandorte/Rome-in-transit@main/wheels/pytz-2023.3-py2.py3-none-any.whl'] for (const pkg of env_spec) { let pkg_name; if (pkg.endsWith('.whl')) { pkg_name = pkg.split('/').slice(-1)[0].split('-')[0] } else { pkg_name = pkg } self.postMessage({type: 'status', msg: `Installing ${pkg_name}`}) try { await self.pyodide.runPythonAsync(` import micropip await micropip.install('${pkg}'); `); } catch(e) { console.log(e) self.postMessage({ type: 'status', msg: `Error while installing ${pkg_name}` }); } } // Load custom Python modules const custom_modules = ['https://raw.githubusercontent.com/ivandorte/Rome-in-transit/main/modules_pyodide/colors.py', 'https://raw.githubusercontent.com/ivandorte/Rome-in-transit/main/modules_pyodide/constants.py', 'https://raw.githubusercontent.com/ivandorte/Rome-in-transit/main/modules_pyodide/indicators.py', 'https://raw.githubusercontent.com/ivandorte/Rome-in-transit/main/modules_pyodide/rome_gtfs_rt.py', 'https://raw.githubusercontent.com/ivandorte/Rome-in-transit/main/modules_pyodide/time_utils.py'] for (const module of custom_modules) { let module_name; module_name = module.split('/').slice(-1)[0] await pyodide.runPythonAsync(` from pyodide.http import pyfetch module_pyodide = await pyfetch('${module}') with open('${module_name}', 'wb') as f: f.write(await module_pyodide.bytes()) `); } console.log("Packages loaded!"); self.postMessage({type: 'status', msg: 'Executing code'}) const code = ` import asyncio from panel.io.pyodide import init_doc, write_doc init_doc() import json import holoviews as hv import numpy as np import panel as pn import requests from bokeh.models import HoverTool from holoviews.streams import Pipe from colors import HEADER_CL from constants import ADMIN_BOUNDS, DASH_DESC from indicators import ( FLEET_IND, IN_TRANSIT_IND, LATE_IND, ON_TIME_IND, STOPPED_IND, ) from rome_gtfs_rt import FULL_DF_SCHEMA, get_data from time_utils import get_current_time from pyodide.http import open_url # Load the bokeh extension hv.extension("bokeh") # Disable webgl: https://github.com/holoviz/panel/issues/4855 hv.renderer("bokeh").webgl = False # Disable Webgl # Set the sizing mode pn.config.sizing_mode = "stretch_both" def init_stream_layers(): """ This function initialize the stream layers """ gtfs_hover = HoverTool( tooltips=[ ("Vehicle ID", "@vehicleID"), ("Trip ID", "@tripID"), ("Start Time", "@startTime"), ("Last Update", "@lastUpdate"), ("Delay (min)", "@delay"), ("Delay Class", "@delayClass"), ("Vehicle Status", "@currentStatusClass"), ] ) status_points = hv.DynamicMap(hv.Points, streams=[gtfs_pipe]) status_points.opts( frame_width=600, frame_height=650, xaxis=None, yaxis=None, color="statusColor", line_alpha=0.0, fill_alpha=0.6, size=6, tools=[gtfs_hover], ) delay_points = hv.DynamicMap(hv.Points, streams=[gtfs_pipe]) delay_points.opts( frame_width=600, frame_height=650, xaxis=None, yaxis=None, color="delayColor", line_alpha=0.0, fill_alpha=0.6, size=6, tools=[gtfs_hover], ) return status_points, delay_points def get_admin_bounds(): """ Returns a Path plot showing the Administrative boundaries of Rome. """ response = open_url(ADMIN_BOUNDS) admin_geojson = json.loads(response.getvalue()) paths = hv.Path([]) for fc in admin_geojson["features"][0]["geometry"]["coordinates"]: fc_path = hv.Path((np.array(fc)[:, 0], np.array(fc)[:, 1])) fc_path.opts(color="grey") paths *= fc_path return paths def update_dashboard(): """ This function updates the Stream Layers and the number widgets """ curr_time = get_current_time() cache_bust = curr_time.split()[-1] data = get_data(cache_bust) if len(data): # Push the data into dynamic maps gtfs_pipe.send(data) # Update the widgets IN_TRANSIT_IND.value = data["currentStatus"].isin([2]).sum(axis=0) STOPPED_IND.value = data["currentStatus"].isin([1]).sum(axis=0) FLEET_IND.value = IN_TRANSIT_IND.value + STOPPED_IND.value ON_TIME_IND.value = data["delayClass"].isin(["On time"]).sum(axis=0) LATE_IND.value = data["delayClass"].isin(["Late"]).sum(axis=0) latest_update_time.value = get_current_time() alert_pane.visible = False else: latest_update_time.value = get_current_time() alert_pane.visible = True # Description pane desc_pane = pn.pane.HTML( DASH_DESC, styles={"text-align": "justified"}, sizing_mode="stretch_width", ) # Latest update time latest_update_time = pn.widgets.StaticText(name="Latest Update") # Indicators status_indicators = pn.Row(IN_TRANSIT_IND, STOPPED_IND, FLEET_IND) delay_indicators = pn.Row(ON_TIME_IND, LATE_IND) # Alert pane alert_pane = pn.pane.Alert("No data received from Roma mobilità!", alert_type="danger") alert_pane.visible = False # Inizialize the pipe gtfs_pipe = Pipe(FULL_DF_SCHEMA) # Inizialize the stream layers status_points, delay_points = init_stream_layers() # CartoLight tiles tiles = hv.element.tiles.CartoLight() # Administrative boundaries of Rome admin_bounds = get_admin_bounds() # Stream layers status_map = tiles * admin_bounds * status_points delay_map = tiles * admin_bounds * delay_points # Initialize the stream layers and indicators update_dashboard() # Define a periodic callback that updates the stream layers and the number widgets every 10 seconds callback = pn.state.add_periodic_callback(callback=update_dashboard, period=10000) # Compose the main layout layout = pn.Row( pn.Column( desc_pane, status_indicators, pn.Spacer(height=5), delay_indicators, pn.Spacer(height=5), latest_update_time, pn.Spacer(height=10), alert_pane, width=400, ), pn.Tabs( ("Vehicle Status", status_map), ("Delays", delay_map), ), ) # Turn into a deployable application pn.template.FastListTemplate( site="", title="Rome in Transit", logo="https://raw.githubusercontent.com/ivandorte/Rome-in-transit/main/assets/train.svg", theme="default", theme_toggle=False, header_background=HEADER_CL, main_max_width="1100px", main=[layout], ).servable() await write_doc() ` try { const [docs_json, render_items, root_ids] = await self.pyodide.runPythonAsync(code) self.postMessage({ type: 'render', docs_json: docs_json, render_items: render_items, root_ids: root_ids }) } catch(e) { const traceback = `${e}` const tblines = traceback.split('\n') self.postMessage({ type: 'status', msg: tblines[tblines.length-2] }); throw e } } self.onmessage = async (event) => { const msg = event.data if (msg.type === 'rendered') { self.pyodide.runPythonAsync(` from panel.io.state import state from panel.io.pyodide import _link_docs_worker _link_docs_worker(state.curdoc, sendPatch, setter='js') `) } else if (msg.type === 'patch') { self.pyodide.globals.set('patch', msg.patch) self.pyodide.runPythonAsync(` state.curdoc.apply_json_patch(patch.to_py(), setter='js') `) self.postMessage({type: 'idle'}) } else if (msg.type === 'location') { self.pyodide.globals.set('location', msg.location) self.pyodide.runPythonAsync(` import json from panel.io.state import state from panel.util import edit_readonly if state.location: loc_data = json.loads(location) with edit_readonly(state.location): state.location.param.update({ k: v for k, v in loc_data.items() if k in state.location.param }) `) } } startApplication()