sibi-dst 0.3.27__py3-none-any.whl → 0.3.29__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sibi_dst/df_helper/__init__.py +2 -0
- sibi_dst/df_helper/_df_helper.py +180 -12
- sibi_dst/df_helper/core/_filter_handler.py +16 -0
- sibi_dst/df_helper/data_cleaner.py +132 -0
- sibi_dst/geopy_helper/__init__.py +7 -0
- sibi_dst/geopy_helper/geo_location_service.py +63 -0
- sibi_dst/geopy_helper/utils.py +55 -0
- sibi_dst/osmnx_helper/__init__.py +9 -0
- sibi_dst/osmnx_helper/base_osm_map.py +165 -0
- sibi_dst/osmnx_helper/basemaps/__init__.py +0 -0
- sibi_dst/osmnx_helper/basemaps/calendar_html.py +122 -0
- sibi_dst/osmnx_helper/basemaps/router_plotter.py +186 -0
- sibi_dst/osmnx_helper/utils.py +267 -0
- sibi_dst/tests/__init__.py +0 -0
- sibi_dst/tests/test_data_wrapper_class.py +78 -0
- {sibi_dst-0.3.27.dist-info → sibi_dst-0.3.29.dist-info}/METADATA +5 -1
- {sibi_dst-0.3.27.dist-info → sibi_dst-0.3.29.dist-info}/RECORD +18 -6
- {sibi_dst-0.3.27.dist-info → sibi_dst-0.3.29.dist-info}/WHEEL +1 -1
@@ -0,0 +1,165 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import html
|
4
|
+
from abc import abstractmethod
|
5
|
+
|
6
|
+
import folium
|
7
|
+
import geopandas as gpd
|
8
|
+
import numpy as np
|
9
|
+
import osmnx as ox
|
10
|
+
from folium.plugins import Fullscreen
|
11
|
+
|
12
|
+
|
13
|
+
class BaseOsmMap:
|
14
|
+
tile_options = {
|
15
|
+
"OpenStreetMap": "OpenStreetMap",
|
16
|
+
"CartoDB": "cartodbpositron",
|
17
|
+
"CartoDB Voyager": "cartodbvoyager"
|
18
|
+
}
|
19
|
+
# Set default bounds for Costa Rica
|
20
|
+
bounds = [[8.0340, -85.9417], [11.2192, -82.5566]]
|
21
|
+
|
22
|
+
def __init__(self, osmnx_graph=None, df=None, **kwargs):
|
23
|
+
if osmnx_graph is None:
|
24
|
+
raise ValueError('osmnx_graph must be provided')
|
25
|
+
if df is None:
|
26
|
+
raise ValueError('df must be provided')
|
27
|
+
if df.empty:
|
28
|
+
raise ValueError('df must not be empty')
|
29
|
+
self.df = df.copy()
|
30
|
+
self.osmnx_graph = osmnx_graph
|
31
|
+
self.lat_col = kwargs.get('lat_col', 'latitude')
|
32
|
+
self.lon_col = kwargs.get('lon_col', 'longitude')
|
33
|
+
self.osm_map = None
|
34
|
+
self.G = None
|
35
|
+
self.map_html_title = self._sanitize_html(kwargs.get('map_html_title', 'OSM Basemap'))
|
36
|
+
|
37
|
+
self.zoom_start = kwargs.pop('zoom_start', 13)
|
38
|
+
self.fullscreen = kwargs.pop('fullscreen', True)
|
39
|
+
self.fullscreen_position = kwargs.pop('fullscreen_position', 'topright')
|
40
|
+
self.tiles = kwargs.pop('tiles', 'OpenStreetMap')
|
41
|
+
self.verbose = kwargs.pop('verbose', False)
|
42
|
+
self.sort_keys = kwargs.pop('sort_keys', None)
|
43
|
+
self.dt_field = kwargs.pop('dt_field', None)
|
44
|
+
self.dt = None
|
45
|
+
self.calc_nearest_nodes = kwargs.pop('calc_nearest_nodes', False)
|
46
|
+
self.nearest_nodes = None
|
47
|
+
self.max_bounds = kwargs.pop('max_bounds', False)
|
48
|
+
self._prepare_df()
|
49
|
+
self._initialise_map()
|
50
|
+
|
51
|
+
|
52
|
+
def _prepare_df(self):
|
53
|
+
if self.sort_keys:
|
54
|
+
self.df.sort_values(by=self.sort_keys, inplace=True)
|
55
|
+
self.df.reset_index(drop=True, inplace=True)
|
56
|
+
self.gps_points = self.df[[self.lat_col, self.lon_col]].values.tolist()
|
57
|
+
if self.dt_field is not None:
|
58
|
+
self.dt = self.df[self.dt_field].tolist()
|
59
|
+
|
60
|
+
if self.calc_nearest_nodes:
|
61
|
+
self.nearest_nodes = ox.distance.nearest_nodes(self.osmnx_graph, X=self.df[self.lon_col],
|
62
|
+
Y=self.df[self.lat_col])
|
63
|
+
|
64
|
+
|
65
|
+
def _initialise_map(self):
|
66
|
+
gps_array = np.array(self.gps_points)
|
67
|
+
mean_latitude = np.mean(gps_array[:, 0])
|
68
|
+
mean_longitude = np.mean(gps_array[:, 1])
|
69
|
+
self.osm_map = folium.Map(location=[mean_latitude, mean_longitude], zoom_start=self.zoom_start,
|
70
|
+
tiles=self.tiles, max_bounds=self.max_bounds)
|
71
|
+
north, south, east, west = self._get_bounding_box_from_points(margin=0.001)
|
72
|
+
self.G = self._extract_subgraph(north, south, east, west)
|
73
|
+
|
74
|
+
|
75
|
+
def _attach_supported_tiles(self):
|
76
|
+
# Normalize the default tile name to lowercase for comparison
|
77
|
+
normalized_default_tile = self.tiles.lower()
|
78
|
+
|
79
|
+
# Filter out the default tile layer from the options to avoid duplication
|
80
|
+
tile_options_filtered = {k: v for k, v in self.tile_options.items() if v.lower() != normalized_default_tile}
|
81
|
+
|
82
|
+
for tile, description in tile_options_filtered.items():
|
83
|
+
folium.TileLayer(name=tile, tiles=description, show=False).add_to(self.osm_map)
|
84
|
+
|
85
|
+
|
86
|
+
def _get_bounding_box_from_points(self, margin=0.001):
|
87
|
+
latitudes = [point[0] for point in self.gps_points]
|
88
|
+
longitudes = [point[1] for point in self.gps_points]
|
89
|
+
|
90
|
+
north = max(latitudes) + margin
|
91
|
+
south = min(latitudes) - margin
|
92
|
+
east = max(longitudes) + margin
|
93
|
+
west = min(longitudes) - margin
|
94
|
+
|
95
|
+
return north, south, east, west
|
96
|
+
|
97
|
+
|
98
|
+
def _extract_subgraph(self, north, south, east, west):
|
99
|
+
# Create a bounding box polygon
|
100
|
+
# from osmnx v2 this is how it is done
|
101
|
+
if ox.__version__ >= '2.0':
|
102
|
+
bbox_poly = gpd.GeoSeries([ox.utils_geo.bbox_to_poly(bbox=(west, south, east, north))])
|
103
|
+
else:
|
104
|
+
bbox_poly = gpd.GeoSeries([ox.utils_geo.bbox_to_poly(north, south, east, west)])
|
105
|
+
|
106
|
+
# Get nodes GeoDataFrame
|
107
|
+
nodes_gdf = ox.graph_to_gdfs(self.osmnx_graph, nodes=True, edges=False)
|
108
|
+
|
109
|
+
# Find nodes within the bounding box
|
110
|
+
nodes_within_bbox = nodes_gdf[nodes_gdf.geometry.within(bbox_poly.geometry.unary_union)]
|
111
|
+
|
112
|
+
# Create subgraph
|
113
|
+
subgraph = self.osmnx_graph.subgraph(nodes_within_bbox.index)
|
114
|
+
|
115
|
+
return subgraph
|
116
|
+
|
117
|
+
|
118
|
+
@abstractmethod
|
119
|
+
def process_map(self):
|
120
|
+
# this is to be implemented at the subclass level
|
121
|
+
# implement here your specific map logic.
|
122
|
+
...
|
123
|
+
|
124
|
+
|
125
|
+
def pre_process_map(self):
|
126
|
+
# this is to be implemented at the subclass level
|
127
|
+
# call super().pre_process_map first to inherit the following behaviour
|
128
|
+
...
|
129
|
+
|
130
|
+
|
131
|
+
def _post_process_map(self):
|
132
|
+
self._attach_supported_tiles()
|
133
|
+
self.add_tile_layer()
|
134
|
+
self._add_fullscreen()
|
135
|
+
self._add_map_title()
|
136
|
+
if self.max_bounds:
|
137
|
+
self.osm_map.fit_bounds(self.bounds)
|
138
|
+
|
139
|
+
|
140
|
+
def add_tile_layer(self):
|
141
|
+
# Override in subclass and call super().add_tile_layer at the end
|
142
|
+
folium.LayerControl().add_to(self.osm_map)
|
143
|
+
|
144
|
+
|
145
|
+
def _add_fullscreen(self):
|
146
|
+
if self.fullscreen:
|
147
|
+
Fullscreen(position=self.fullscreen_position).add_to(self.osm_map)
|
148
|
+
|
149
|
+
|
150
|
+
def _add_map_title(self):
|
151
|
+
if self.map_html_title:
|
152
|
+
self.osm_map.get_root().html.add_child(folium.Element(self.map_html_title))
|
153
|
+
|
154
|
+
|
155
|
+
@staticmethod
|
156
|
+
def _sanitize_html(input_html):
|
157
|
+
return html.escape(input_html)
|
158
|
+
|
159
|
+
|
160
|
+
def generate_map(self):
|
161
|
+
self.pre_process_map()
|
162
|
+
self.process_map()
|
163
|
+
self._post_process_map()
|
164
|
+
|
165
|
+
return self.osm_map
|
File without changes
|
@@ -0,0 +1,122 @@
|
|
1
|
+
|
2
|
+
# HTML and CSS for the calendar button and popup
|
3
|
+
calendar_html = """
|
4
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
|
5
|
+
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
6
|
+
|
7
|
+
<style>
|
8
|
+
/* Style for the calendar button */
|
9
|
+
.calendar-btn {
|
10
|
+
background-color: white;
|
11
|
+
border: 1px solid gray;
|
12
|
+
border-radius: 3px;
|
13
|
+
padding: 5px;
|
14
|
+
font-size: 16px;
|
15
|
+
cursor: pointer;
|
16
|
+
position: fixed; /* Changed from absolute to fixed */
|
17
|
+
bottom: 50px; /* Adjust position relative to the viewport */
|
18
|
+
left: 10px; /* Adjust position relative to the viewport */
|
19
|
+
z-index: 10000; /* Ensure it stays on top of other elements */
|
20
|
+
}
|
21
|
+
|
22
|
+
/* Calendar popup with sufficient size */
|
23
|
+
.calendar-popup {
|
24
|
+
display: none;
|
25
|
+
position: fixed; /* Keep the popup fixed so it stays in view */
|
26
|
+
bottom: 100px;
|
27
|
+
left: 10px;
|
28
|
+
background-color: white;
|
29
|
+
padding: 10px;
|
30
|
+
border: 1px solid gray;
|
31
|
+
border-radius: 3px;
|
32
|
+
z-index: 10000; /* Ensure it stays on top of other elements */
|
33
|
+
width: 250px;
|
34
|
+
height: 300px;
|
35
|
+
}
|
36
|
+
|
37
|
+
/* Ensure the calendar fits properly */
|
38
|
+
#calendar {
|
39
|
+
width: 100%;
|
40
|
+
height: auto;
|
41
|
+
}
|
42
|
+
</style>
|
43
|
+
|
44
|
+
<!-- Calendar Button -->
|
45
|
+
<div class="calendar-btn">📅 Select Date</div>
|
46
|
+
|
47
|
+
<!-- Calendar Popup -->
|
48
|
+
<div class="calendar-popup" id="calendar-popup">
|
49
|
+
<div id="calendar"></div>
|
50
|
+
</div>
|
51
|
+
|
52
|
+
<script>
|
53
|
+
// Initialize Flatpickr calendar
|
54
|
+
const today = new Date().toISOString().split('T')[0];
|
55
|
+
// Function to show the "Please wait" message
|
56
|
+
function showLoadingMessage() {
|
57
|
+
let loadingMessage = document.createElement("div");
|
58
|
+
loadingMessage.id = "loading-message";
|
59
|
+
loadingMessage.style.position = "fixed";
|
60
|
+
loadingMessage.style.top = "50%";
|
61
|
+
loadingMessage.style.left = "50%";
|
62
|
+
loadingMessage.style.transform = "translate(-50%, -50%)";
|
63
|
+
loadingMessage.style.backgroundColor = "rgba(0, 0, 0, 0.8)";
|
64
|
+
loadingMessage.style.color = "white";
|
65
|
+
loadingMessage.style.padding = "20px";
|
66
|
+
loadingMessage.style.borderRadius = "5px";
|
67
|
+
loadingMessage.style.zIndex = "9999";
|
68
|
+
loadingMessage.innerText = "Please wait...";
|
69
|
+
document.body.appendChild(loadingMessage);
|
70
|
+
}
|
71
|
+
|
72
|
+
// Function to remove the "Please wait" message
|
73
|
+
function removeLoadingMessage() {
|
74
|
+
let loadingMessage = document.getElementById("loading-message");
|
75
|
+
if (loadingMessage) {
|
76
|
+
loadingMessage.remove();
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
|
81
|
+
flatpickr("#calendar", {
|
82
|
+
inline: true, // Render the calendar inline within the container
|
83
|
+
maxDate: today, // Disable future dates
|
84
|
+
onChange: function(selectedDates, dateStr, instance) {
|
85
|
+
console.log("Selected date: " + dateStr); // Debugging: Log the selected date
|
86
|
+
// Get the current URL and create a URL object to manipulate the query parameters
|
87
|
+
// Get the current URL from the parent window
|
88
|
+
showLoadingMessage();
|
89
|
+
let currentUrl = window.parent.location.href;
|
90
|
+
|
91
|
+
// If the URL contains "srcdoc", remove it and use the correct base path
|
92
|
+
if (currentUrl.includes("srcdoc")) {
|
93
|
+
currentUrl = currentUrl.replace("srcdoc", "");
|
94
|
+
}
|
95
|
+
|
96
|
+
const url = new URL(currentUrl);
|
97
|
+
|
98
|
+
// Set or update the 'date' parameter while preserving existing parameters
|
99
|
+
url.searchParams.set('date', dateStr);
|
100
|
+
|
101
|
+
console.log("Updated URL: " + url.toString()); // Debugging: Log the updated URL
|
102
|
+
|
103
|
+
// Update the parent window's location with the new URL
|
104
|
+
window.parent.location.href = url.toString();
|
105
|
+
}
|
106
|
+
});
|
107
|
+
// Remove the "Please wait" message once the page has finished loading
|
108
|
+
window.addEventListener("load", function() {
|
109
|
+
removeLoadingMessage();
|
110
|
+
});
|
111
|
+
|
112
|
+
// Toggle the calendar popup when the button is clicked
|
113
|
+
document.querySelector(".calendar-btn").addEventListener("click", function() {
|
114
|
+
var popup = document.getElementById("calendar-popup");
|
115
|
+
if (popup.style.display === "none" || popup.style.display === "") {
|
116
|
+
popup.style.display = "block";
|
117
|
+
} else {
|
118
|
+
popup.style.display = "none";
|
119
|
+
}
|
120
|
+
});
|
121
|
+
</script>
|
122
|
+
"""
|
@@ -0,0 +1,186 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from sibi_dst.osmnx_helper.utils import get_distance_between_points, add_arrows
|
3
|
+
from collections import defaultdict
|
4
|
+
import folium
|
5
|
+
from folium.plugins import AntPath
|
6
|
+
import networkx as nx
|
7
|
+
|
8
|
+
from sibi_dst.osmnx_helper import BaseOsmMap
|
9
|
+
from sibi_dst.osmnx_helper.basemaps.calendar_html import calendar_html
|
10
|
+
|
11
|
+
class RoutePlotter(BaseOsmMap):
|
12
|
+
def __init__(self, osmnx_graph, df, **kwargs):
|
13
|
+
self.action_field = kwargs.pop('action_field', '')
|
14
|
+
self.action_groups = kwargs.pop('action_groups', {})
|
15
|
+
self.action_styles = kwargs.pop('action_styles', {})
|
16
|
+
self.use_ant_path = kwargs.pop('use_ant_path', True)
|
17
|
+
self.show_calendar = kwargs.pop('show_calendar', True)
|
18
|
+
self.show_map_title = kwargs.pop('show_map_title', True)
|
19
|
+
self.sort_keys = kwargs.pop('sort_keys', None)
|
20
|
+
self.main_route_layer = folium.FeatureGroup(name="Main Route")
|
21
|
+
self.feature_groups = {}
|
22
|
+
self.feature_group_counts = {}
|
23
|
+
self.total_distance = 0.0
|
24
|
+
self.actions = []
|
25
|
+
self.action_group_counts = {action_group: 0 for action_group in self.action_groups}
|
26
|
+
self.marker_count = 1
|
27
|
+
kwargs.update({'calc_nearest_nodes': True})
|
28
|
+
kwargs['dt_field'] = 'date_time'
|
29
|
+
super().__init__(osmnx_graph, df, **kwargs)
|
30
|
+
|
31
|
+
def pre_process_map(self):
|
32
|
+
super().pre_process_map()
|
33
|
+
self.actions = self.df[self.action_field].tolist()
|
34
|
+
|
35
|
+
def process_map(self):
|
36
|
+
self._calculate_routes()
|
37
|
+
self._plot_routes()
|
38
|
+
self._add_markers()
|
39
|
+
self.main_route_layer.add_to(self.osm_map)
|
40
|
+
if self.show_calendar:
|
41
|
+
self._add_calendar()
|
42
|
+
|
43
|
+
def _calculate_routes(self):
|
44
|
+
if self.verbose:
|
45
|
+
print("Calculating routes and markers...")
|
46
|
+
distances = [
|
47
|
+
get_distance_between_points(tuple(self.gps_points[0]), tuple(coord), 'm')
|
48
|
+
for coord in self.gps_points
|
49
|
+
]
|
50
|
+
self.max_distance_index = distances.index(max(distances))
|
51
|
+
self.max_time_index = self.dt.index(max(self.dt))
|
52
|
+
self.route_polylines = []
|
53
|
+
self.markers = defaultdict(list) # Store markers for action groups
|
54
|
+
for i in range(len(self.gps_points) - 1):
|
55
|
+
polyline, color, markers = self._calculate_route(i)
|
56
|
+
if polyline:
|
57
|
+
self.route_polylines.append((polyline, color))
|
58
|
+
for action_group, action_markers in markers.items():
|
59
|
+
self.markers[action_group].extend(action_markers)
|
60
|
+
self.action_group_counts[action_group] += 1
|
61
|
+
self.marker_count += 1
|
62
|
+
if self.verbose:
|
63
|
+
print("Route and marker calculation complete.")
|
64
|
+
|
65
|
+
for action_group in self.action_groups:
|
66
|
+
count = self.action_group_counts[action_group]
|
67
|
+
self.feature_groups[action_group] = folium.FeatureGroup(name=f"{action_group} ({count})").add_to(
|
68
|
+
self.osm_map)
|
69
|
+
self.osm_map.add_child(self.feature_groups[action_group])
|
70
|
+
|
71
|
+
def _calculate_route(self, i):
|
72
|
+
if self.verbose:
|
73
|
+
print(f"Calculating for item:{i}")
|
74
|
+
orig = self.nearest_nodes[i]
|
75
|
+
dest = self.nearest_nodes[i + 1]
|
76
|
+
try:
|
77
|
+
route = nx.shortest_path(self.G, orig, dest, weight='length')
|
78
|
+
route_length = sum(self.G[u][v][0]['length'] for u, v in zip(route[:-1], route[1:]))
|
79
|
+
self.total_distance += route_length
|
80
|
+
offset = 0 if i < self.max_distance_index else 0.0005
|
81
|
+
lats, lons = zip(*[(self.G.nodes[node]['y'] + offset, self.G.nodes[node]['x']) for node in route])
|
82
|
+
color = 'blue' if i < self.max_distance_index else 'red'
|
83
|
+
polyline = list(zip(lats, lons))
|
84
|
+
markers = self._calculate_markers(i)
|
85
|
+
return polyline, color, markers
|
86
|
+
except nx.NetworkXNoPath:
|
87
|
+
if self.verbose:
|
88
|
+
print(f"Item:{i}-No path found for {orig} to {dest}")
|
89
|
+
return None, None, {}
|
90
|
+
except nx.NodeNotFound:
|
91
|
+
if self.verbose:
|
92
|
+
print(f"Item:{i}-No path found for {orig} to {dest}")
|
93
|
+
return None, None, {}
|
94
|
+
|
95
|
+
def _calculate_markers(self, i):
|
96
|
+
# Calculate markers for action groups
|
97
|
+
markers = defaultdict(list)
|
98
|
+
for action_group in self.action_groups:
|
99
|
+
action_indices = [idx for idx, action in enumerate(self.actions) if action == action_group]
|
100
|
+
for idx in action_indices:
|
101
|
+
if idx == i:
|
102
|
+
location = self.gps_points[i]
|
103
|
+
tooltip = f"Result {self.marker_count}: {action_group}<br>Date/time:{self.dt[i]}"
|
104
|
+
popup_data = self._get_data(i)
|
105
|
+
action_style = self.action_styles.get(action_group,
|
106
|
+
{'color': 'blue', 'icon': 'marker', 'prefix': 'fa'})
|
107
|
+
markers[action_group].append((location, tooltip, popup_data, action_style))
|
108
|
+
return markers
|
109
|
+
|
110
|
+
def _plot_routes(self):
|
111
|
+
if self.verbose:
|
112
|
+
print("Plotting routes and markers...")
|
113
|
+
# self.action_group_counts = {action_group: 0 for action_group in self.feature_groups.keys()}
|
114
|
+
for polyline, color in self.route_polylines:
|
115
|
+
if self.use_ant_path:
|
116
|
+
AntPath(
|
117
|
+
locations=polyline,
|
118
|
+
color=color,
|
119
|
+
weight=3, # Increase line thickness
|
120
|
+
opacity=10, # Increase opacity
|
121
|
+
# pulse_color=color,
|
122
|
+
delay=1000, # Slower animation to reduce flickering
|
123
|
+
# dash_array=[20, 30] # Adjust dash pattern if needed
|
124
|
+
).add_to(self.main_route_layer)
|
125
|
+
else:
|
126
|
+
folium.PolyLine(locations=polyline, color=color).add_to(self.main_route_layer)
|
127
|
+
self.osm_map = add_arrows(self.osm_map, polyline, color, n_arrows=3)
|
128
|
+
# Plot markers for action groups
|
129
|
+
for action_group, action_markers in self.markers.items():
|
130
|
+
for location, tooltip, popup_data, action_style in action_markers:
|
131
|
+
folium.Marker(
|
132
|
+
location=location,
|
133
|
+
popup=folium.Popup(popup_data, max_width=600),
|
134
|
+
tooltip=tooltip,
|
135
|
+
icon=folium.Icon(
|
136
|
+
icon=action_style.get("icon"),
|
137
|
+
color=action_style.get("color"),
|
138
|
+
prefix=action_style.get("prefix")
|
139
|
+
)
|
140
|
+
).add_to(self.feature_groups[action_group])
|
141
|
+
|
142
|
+
if self.verbose:
|
143
|
+
print("Route and marker plotting complete.")
|
144
|
+
|
145
|
+
def _add_markers(self):
|
146
|
+
if self.verbose:
|
147
|
+
print("Adding markers...")
|
148
|
+
# Add start marker
|
149
|
+
start_popup = folium.Popup(f"Start of route at {self.dt[0]}", max_width=300)
|
150
|
+
folium.Marker(location=self.gps_points[0], popup=start_popup,
|
151
|
+
icon=folium.Icon(icon='flag-checkered', prefix='fa')).add_to(self.osm_map)
|
152
|
+
# Add total distance marker at the end
|
153
|
+
folium.Marker(
|
154
|
+
self.gps_points[-1],
|
155
|
+
popup=f"End of Route at {self.dt[self.max_time_index]}. Total Distance Travelled: {self.total_distance / 1000:.2f} km",
|
156
|
+
icon=folium.Icon(color="red", icon="flag-checkered", prefix="fa")
|
157
|
+
).add_to(self.osm_map)
|
158
|
+
if self.verbose:
|
159
|
+
print("Marker addition complete.")
|
160
|
+
|
161
|
+
def _add_calendar(self):
|
162
|
+
calendar_element = folium.Element(calendar_html)
|
163
|
+
self.osm_map.get_root().html.add_child(calendar_element)
|
164
|
+
|
165
|
+
def _add_map_title(self):
|
166
|
+
if self.map_html_title and self.show_map_title:
|
167
|
+
title_html = f'''
|
168
|
+
<div style="position: fixed;
|
169
|
+
top: 10px;
|
170
|
+
left: 50%;
|
171
|
+
transform: translate(-50%, 0%);
|
172
|
+
z-index: 9999;
|
173
|
+
font-size: 24px;
|
174
|
+
font-weight: bold;
|
175
|
+
background-color: white;
|
176
|
+
padding: 10px;
|
177
|
+
border: 2px solid black;
|
178
|
+
border-radius: 5px;">
|
179
|
+
{self.map_html_title}
|
180
|
+
</div>
|
181
|
+
'''
|
182
|
+
self.osm_map.get_root().html.add_child(folium.Element(title_html))
|
183
|
+
|
184
|
+
def _get_data(self, index):
|
185
|
+
# implement in subclass to populate popups
|
186
|
+
...
|