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.
@@ -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
+ ...