sibi-dst 0.3.31__py3-none-any.whl → 0.3.33__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/_parquet_artifact.py +68 -0
- sibi_dst/df_helper/_parquet_reader.py +45 -1
- sibi_dst/df_helper/backends/django/_db_connection.py +41 -1
- sibi_dst/df_helper/backends/django/_io_dask.py +211 -3
- sibi_dst/df_helper/backends/django/_load_from_db.py +96 -1
- sibi_dst/df_helper/backends/django/_sql_model_builder.py +132 -6
- sibi_dst/df_helper/backends/http/_http_config.py +52 -1
- sibi_dst/df_helper/backends/parquet/_filter_handler.py +28 -0
- sibi_dst/df_helper/backends/parquet/_parquet_options.py +105 -1
- sibi_dst/df_helper/backends/sqlalchemy/_db_connection.py +17 -0
- sibi_dst/df_helper/backends/sqlalchemy/_load_from_db.py +80 -2
- sibi_dst/df_helper/backends/sqlalchemy/_sql_model_builder.py +90 -29
- sibi_dst/df_helper/core/_params_config.py +59 -0
- sibi_dst/geopy_helper/geo_location_service.py +14 -0
- sibi_dst/geopy_helper/utils.py +37 -3
- sibi_dst/osmnx_helper/base_osm_map.py +254 -0
- sibi_dst/osmnx_helper/utils.py +226 -4
- sibi_dst/utils/clickhouse_writer.py +27 -0
- sibi_dst/utils/data_utils.py +32 -1
- sibi_dst/utils/data_wrapper.py +94 -6
- sibi_dst/utils/date_utils.py +35 -0
- sibi_dst/utils/log_utils.py +19 -2
- sibi_dst/utils/parquet_saver.py +1 -0
- sibi_dst/utils/storage_manager.py +4 -1
- {sibi_dst-0.3.31.dist-info → sibi_dst-0.3.33.dist-info}/METADATA +3 -1
- {sibi_dst-0.3.31.dist-info → sibi_dst-0.3.33.dist-info}/RECORD +27 -27
- {sibi_dst-0.3.31.dist-info → sibi_dst-0.3.33.dist-info}/WHEEL +0 -0
@@ -10,6 +10,28 @@ apps_label = "datacubes"
|
|
10
10
|
|
11
11
|
|
12
12
|
class SqlAlchemyModelBuilder:
|
13
|
+
"""
|
14
|
+
Provides functionality for building SQLAlchemy ORM models dynamically from
|
15
|
+
reflected database tables. This class is intended for use with a SQLAlchemy
|
16
|
+
engine and metadata to automatically generate ORM models for specified
|
17
|
+
database tables.
|
18
|
+
|
19
|
+
The primary purpose of this class is to simplify the process of creating
|
20
|
+
SQLAlchemy ORM models by reflecting tables from a connected database,
|
21
|
+
dynamically generating model classes, and handling relationships between
|
22
|
+
tables.
|
23
|
+
|
24
|
+
:ivar engine: SQLAlchemy engine connected to the database.
|
25
|
+
:type engine: Engine
|
26
|
+
:ivar table_name: Name of the table for which the model is generated.
|
27
|
+
:type table_name: str
|
28
|
+
:ivar metadata: SQLAlchemy MetaData instance for reflecting tables.
|
29
|
+
:type metadata: MetaData
|
30
|
+
:ivar table: Reflected SQLAlchemy Table object for the specified table name.
|
31
|
+
:type table: Optional[Table]
|
32
|
+
:ivar class_name: Dynamically normalized class name derived from table_name.
|
33
|
+
:type class_name: str
|
34
|
+
"""
|
13
35
|
_model_cache = {} # Local cache for model classes
|
14
36
|
|
15
37
|
def __init__(self, engine, table_name):
|
@@ -27,6 +49,16 @@ class SqlAlchemyModelBuilder:
|
|
27
49
|
self.class_name = self.normalize_class_name(self.table_name)
|
28
50
|
|
29
51
|
def build_model(self) -> type:
|
52
|
+
"""
|
53
|
+
Builds and returns a database model class corresponding to the specified table name.
|
54
|
+
The method checks if the model is already registered in the ORM's registry. If not,
|
55
|
+
it reflects the database schema of the specified table and dynamically creates the
|
56
|
+
model class.
|
57
|
+
|
58
|
+
:raises ValueError: If the specified table does not exist in the database.
|
59
|
+
:return: A database model class corresponding to the specified table name.
|
60
|
+
:rtype: type
|
61
|
+
"""
|
30
62
|
# Check if the model is already registered
|
31
63
|
model = Base.registry._class_registry.get(self.class_name)
|
32
64
|
if model:
|
@@ -42,10 +74,17 @@ class SqlAlchemyModelBuilder:
|
|
42
74
|
|
43
75
|
def create_model(self) -> type:
|
44
76
|
"""
|
45
|
-
|
77
|
+
Generates a SQLAlchemy model class dynamically based on the specified table and
|
78
|
+
its columns. The method extracts column information, defines the necessary
|
79
|
+
attributes, and creates the model class if it doesn't already exist in the
|
80
|
+
SQLAlchemy base registry.
|
46
81
|
|
47
|
-
|
48
|
-
|
82
|
+
:raises KeyError: If the table or table name does not exist in the provided
|
83
|
+
schema.
|
84
|
+
:raises Exception: If the model creation fails for any reason.
|
85
|
+
|
86
|
+
:return: The dynamically created or fetched model class.
|
87
|
+
:rtype: type
|
49
88
|
"""
|
50
89
|
# Normalize the class name from the table name
|
51
90
|
columns = self.get_columns(self.table)
|
@@ -70,13 +109,17 @@ class SqlAlchemyModelBuilder:
|
|
70
109
|
|
71
110
|
def get_columns(self, table: Table):
|
72
111
|
"""
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
112
|
+
Extracts and returns a dictionary of column names and their corresponding column
|
113
|
+
objects from a given table, excluding reserved names. Reserved names are used
|
114
|
+
internally and should not overlap with column names in the provided table. The
|
115
|
+
method ensures sanitized column names through normalization and filters out any
|
116
|
+
column matching reserved keywords.
|
117
|
+
|
118
|
+
:param table: The table object from which columns are to be extracted.
|
119
|
+
:type table: Table
|
120
|
+
:return: A dictionary containing the sanitized column names as keys and their
|
121
|
+
corresponding column objects as values, excluding reserved names.
|
122
|
+
:rtype: dict
|
80
123
|
"""
|
81
124
|
columns = {}
|
82
125
|
reserved_names = ["metadata", "class_", "table"]
|
@@ -89,11 +132,18 @@ class SqlAlchemyModelBuilder:
|
|
89
132
|
|
90
133
|
def add_relationships(self, attrs, table: Table):
|
91
134
|
"""
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
135
|
+
Adds relationships to the provided attributes dictionary for a given database table.
|
136
|
+
|
137
|
+
This method iterates through the foreign keys of the provided table, constructs
|
138
|
+
relationship attributes, and updates the attributes dictionary with relationships
|
139
|
+
that connect the current table to related tables.
|
140
|
+
|
141
|
+
:param attrs: Dictionary of attributes to which relationships will be added.
|
142
|
+
The dictionary will be updated with new relationship mappings.
|
143
|
+
:type attrs: dict
|
144
|
+
:param table: A database table object containing foreign key relationships.
|
145
|
+
The method will use this table to establish relationships.
|
146
|
+
:return: None
|
97
147
|
"""
|
98
148
|
for fk in table.foreign_keys:
|
99
149
|
related_table_name = fk.column.table.name
|
@@ -104,26 +154,37 @@ class SqlAlchemyModelBuilder:
|
|
104
154
|
@staticmethod
|
105
155
|
def normalize_class_name(table_name: str) -> str:
|
106
156
|
"""
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
157
|
+
Generate a normalized class name from a given table name by capitalizing
|
158
|
+
each word separated by underscores and concatenating them.
|
159
|
+
|
160
|
+
This static method takes a string representation of a table name, where
|
161
|
+
words are separated by underscores, and converts it into a camel case
|
162
|
+
class name. It processes the string by capitalizing the first letter of
|
163
|
+
each word and removing the underscores. The normalized class name
|
164
|
+
returned can be used programmatically for various purposes, such as
|
165
|
+
class generation or naming conventions.
|
166
|
+
|
167
|
+
:param table_name: The table name to normalize, with words separated by
|
168
|
+
underscores. E.g., 'sample_table' becomes 'SampleTable'.
|
169
|
+
:type table_name: str
|
170
|
+
:return: A normalized class name in camel case format.
|
171
|
+
:rtype: str
|
114
172
|
"""
|
115
173
|
return "".join(word.capitalize() for word in table_name.split("_"))
|
116
174
|
|
117
175
|
@staticmethod
|
118
176
|
def normalize_column_name(column_name: str) -> str:
|
119
177
|
"""
|
120
|
-
Normalize a column name
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
178
|
+
Normalize a column name by replacing any non-word characters or leading numbers
|
179
|
+
with underscores, while ensuring it does not conflict with reserved keywords
|
180
|
+
such as 'class', 'def', 'return', etc. If the normalized name conflicts with
|
181
|
+
a Python reserved keyword, "_field" is appended to it.
|
182
|
+
|
183
|
+
:param column_name: The original name of the column to be normalized.
|
184
|
+
:type column_name: str
|
185
|
+
:return: A normalized column name that is safe and compatible for usage
|
186
|
+
in various contexts such as database columns or Python code.
|
187
|
+
:rtype: str
|
127
188
|
"""
|
128
189
|
column_name = re.sub(r"\W|^(?=\d)", "_", column_name)
|
129
190
|
if column_name in {"class", "def", "return", "yield", "global"}:
|
@@ -27,6 +27,36 @@ LOOKUP_SEP = "__"
|
|
27
27
|
|
28
28
|
|
29
29
|
class ParamsConfig(BaseModel):
|
30
|
+
"""
|
31
|
+
Defines a configuration model for parameters with functionality for parsing,
|
32
|
+
validation, and conversion of legacy filters.
|
33
|
+
|
34
|
+
This class extends BaseModel from Pydantic and is designed to handle multiple
|
35
|
+
sets of configurations, including field mappings, filters, dataframe parameters,
|
36
|
+
and dataframe options. It allows for flexible parsing of parameters across a
|
37
|
+
variety of supported structures and ensures that legacy filters can be
|
38
|
+
appropriately converted for compatibility.
|
39
|
+
|
40
|
+
:ivar field_map: Maps field names to their equivalent legacy field names.
|
41
|
+
:type field_map: Optional[Dict]
|
42
|
+
:ivar legacy_filters: Indicates whether legacy filters should be processed.
|
43
|
+
:type legacy_filters: bool
|
44
|
+
:ivar sticky_filters: Stores additional filters as key-value pairs that persist
|
45
|
+
across parameter parsing.
|
46
|
+
:type sticky_filters: Dict[str, Union[str, bool, int, float, list, tuple]]
|
47
|
+
:ivar filters: Holds all the current filters including sticky and dynamically
|
48
|
+
parsed filters.
|
49
|
+
:type filters: Dict[str, Union[str, Dict, bool, int, float, list, tuple]]
|
50
|
+
:ivar df_params: Contains parameters related to dataframe configurations in a
|
51
|
+
structured format.
|
52
|
+
:type df_params: Dict[str, Union[tuple, str, bool, None]]
|
53
|
+
:ivar df_options: Stores optional configurations for a dataframe, allowing for
|
54
|
+
additional behavior customization.
|
55
|
+
:type df_options: Dict[str, Union[bool, str, None]]
|
56
|
+
:ivar params: Dictionary of parameters provided for configuration, supporting
|
57
|
+
both basic and nested structures.
|
58
|
+
:type params: Dict[str, Union[str, bool, int, float, List[Union[str, int, bool, float]]]]
|
59
|
+
"""
|
30
60
|
field_map: Optional[Dict] = Field(default_factory=dict)
|
31
61
|
legacy_filters: bool = False
|
32
62
|
sticky_filters: Dict[str, Union[str, bool, int, float, list, tuple]] = Field(default_factory=dict)
|
@@ -42,6 +72,17 @@ class ParamsConfig(BaseModel):
|
|
42
72
|
return self
|
43
73
|
|
44
74
|
def parse_params(self, params):
|
75
|
+
"""
|
76
|
+
Parses and separates the given parameters into specific categories such as dataframe parameters,
|
77
|
+
dataframe options, and filters. Updates existing class attributes with the parsed values,
|
78
|
+
retaining any sticky filters. Also handles the legacy filters if provided.
|
79
|
+
|
80
|
+
:param params: Dictionary containing parameters to process. These parameters can include specific
|
81
|
+
keys relevant for dataframe configuration (e.g., dataframe parameters, dataframe options)
|
82
|
+
as well as arbitrary filter settings.
|
83
|
+
:type params: dict
|
84
|
+
:return: None
|
85
|
+
"""
|
45
86
|
self.legacy_filters = params.pop('legacy_filters', self.legacy_filters)
|
46
87
|
self.field_map = params.pop('field_map', self.field_map)
|
47
88
|
self.sticky_filters = params.pop('params', self.sticky_filters)
|
@@ -60,6 +101,24 @@ class ParamsConfig(BaseModel):
|
|
60
101
|
self.convert_legacy_filters()
|
61
102
|
|
62
103
|
def convert_legacy_filters(self):
|
104
|
+
"""
|
105
|
+
Converts legacy filter fields in the `self.filters` dictionary to their
|
106
|
+
modern equivalents using the mappings provided in `self.field_map`.
|
107
|
+
This method ensures backward compatibility for filters by automatically
|
108
|
+
translating the old field names into the current system.
|
109
|
+
|
110
|
+
The function first verifies that the required dictionaries (`legacy_filters`,
|
111
|
+
`field_map`, `filters`) are valid. It creates a reverse map of `field_map` for
|
112
|
+
efficient lookup, processes the key names within `self.filters`, and updates
|
113
|
+
them to reflect the legacy mapping.
|
114
|
+
|
115
|
+
:raises KeyError: If any required dictionary key is missing during processing.
|
116
|
+
|
117
|
+
:param self.legacy_filters: A boolean flag indicating whether legacy filters
|
118
|
+
are being used.
|
119
|
+
:type self.legacy_filters: bool
|
120
|
+
|
121
|
+
"""
|
63
122
|
if not self.legacy_filters or not self.field_map or not self.filters:
|
64
123
|
return
|
65
124
|
# create a reverse map of the field_map
|
@@ -8,6 +8,20 @@ app_geo_locator_test_place = os.environ.get('GEO_LOCATOR_TEST_PLACE', "San Jose,
|
|
8
8
|
|
9
9
|
|
10
10
|
class GeolocationService:
|
11
|
+
"""
|
12
|
+
Provides geolocation services, such as forward and reverse geocoding.
|
13
|
+
|
14
|
+
This class is intended to interface with a geocoding service (e.g., Nominatim)
|
15
|
+
for performing geocoding operations. It initializes the geolocation service
|
16
|
+
based on a provided or default configuration and provides methods for geocoding
|
17
|
+
addresses or retrieving addresses from coordinates.
|
18
|
+
|
19
|
+
:ivar geolocator: Instance of the geocoding service used for geolocation tasks.
|
20
|
+
Will be `None` if initialization fails or is incomplete.
|
21
|
+
:type geolocator: Optional[Nominatim]
|
22
|
+
:ivar debug: Indicates whether debug messages are enabled.
|
23
|
+
:type debug: bool
|
24
|
+
"""
|
11
25
|
debug: bool = False
|
12
26
|
|
13
27
|
def __init__(self, debug=False):
|
sibi_dst/geopy_helper/utils.py
CHANGED
@@ -5,6 +5,16 @@ geolocator = None
|
|
5
5
|
|
6
6
|
|
7
7
|
def get_geolocator():
|
8
|
+
"""
|
9
|
+
Provides a function to instantiate or retrieve a global geolocator instance
|
10
|
+
using the GeolocationService class. If the geolocator has already been
|
11
|
+
created, it will return the original global instance. Otherwise, it initializes
|
12
|
+
a new instance of the GeolocationService with debugging enabled and stores it
|
13
|
+
globally.
|
14
|
+
|
15
|
+
:return: The global instance of the GeolocationService
|
16
|
+
:rtype: GeolocationService
|
17
|
+
"""
|
8
18
|
global geolocator
|
9
19
|
if geolocator is None:
|
10
20
|
geolocator = GeolocationService(debug=True)
|
@@ -15,6 +25,23 @@ def get_geolocator():
|
|
15
25
|
|
16
26
|
|
17
27
|
def get_address_by_coordinates(latitude, longitude, exactly_one=True):
|
28
|
+
"""
|
29
|
+
Retrieves the address based on the provided geographic coordinates (latitude and
|
30
|
+
longitude). Utilizes the geopy library's geolocator to find and reverse-geocode
|
31
|
+
the location associated with the given coordinates. Returns a human-readable
|
32
|
+
address if available or an error message for specific conditions.
|
33
|
+
|
34
|
+
:param latitude: The latitude of the location to find the address for.
|
35
|
+
:type latitude: float
|
36
|
+
:param longitude: The longitude of the location to find the address for.
|
37
|
+
:type longitude: float
|
38
|
+
:param exactly_one: If true, ensures exactly one result is returned. If false,
|
39
|
+
returns a list of possible matches. Defaults to True.
|
40
|
+
:type exactly_one: bool, optional
|
41
|
+
:return: A string containing the human-readable address of the location or an
|
42
|
+
error message in case of failure.
|
43
|
+
:rtype: str
|
44
|
+
"""
|
18
45
|
geolocator = get_geolocator()
|
19
46
|
try:
|
20
47
|
location = geolocator.reverse((latitude, longitude), exactly_one=exactly_one)
|
@@ -28,10 +55,17 @@ def get_address_by_coordinates(latitude, longitude, exactly_one=True):
|
|
28
55
|
|
29
56
|
def get_coordinates_for_address(address):
|
30
57
|
"""
|
31
|
-
|
58
|
+
Gets geographical coordinates (latitude and longitude) along with the full formatted
|
59
|
+
address for a given address string. Makes use of a geolocation service to retrieve
|
60
|
+
the data and handles possible exceptions during the process.
|
61
|
+
|
62
|
+
:param address: The address as a string for which coordinates need to be determined.
|
63
|
+
:type address: str
|
32
64
|
|
33
|
-
:
|
34
|
-
|
65
|
+
:return: A dictionary containing the full formatted address, latitude, and longitude
|
66
|
+
if the location is found. Otherwise, returns a string describing an error
|
67
|
+
or that no location was found.
|
68
|
+
:rtype: dict or str
|
35
69
|
"""
|
36
70
|
geolocator = get_geolocator()
|
37
71
|
try:
|
@@ -11,6 +11,64 @@ from folium.plugins import Fullscreen
|
|
11
11
|
|
12
12
|
|
13
13
|
class BaseOsmMap:
|
14
|
+
"""
|
15
|
+
BaseOsmMap class serves as a foundational class for creating and managing maps
|
16
|
+
using OpenStreetMap data. It integrates features from libraries like osmnx and
|
17
|
+
folium to facilitate visualization, tile layer management, bounding box handling,
|
18
|
+
and nearest node calculation for geographic coordinates.
|
19
|
+
|
20
|
+
The class is designed to be subclassed, enabling customization of map processing
|
21
|
+
logic while providing core functionalities needed for map creation and manipulation.
|
22
|
+
|
23
|
+
:ivar tile_options: Dictionary containing options for pre-defined tile layers.
|
24
|
+
Keys represent display names while values represent tile layer configurations.
|
25
|
+
:type tile_options: dict
|
26
|
+
:ivar bounds: Default map bounds representing geographical extents for Costa Rica.
|
27
|
+
:type bounds: list[list[float]]
|
28
|
+
:ivar osmnx_graph: Input graph representing OpenStreetMap network data, used for
|
29
|
+
operations like subgraph extraction and nearest node calculation.
|
30
|
+
:type osmnx_graph: networkx.classes.multidigraph.MultiDiGraph
|
31
|
+
:ivar df: DataFrame containing data points with geographic coordinates. It must
|
32
|
+
not be empty and should include latitude and longitude columns as specified
|
33
|
+
in `lat_col` and `lon_col`.
|
34
|
+
:type df: pandas.DataFrame
|
35
|
+
:ivar lat_col: Name of the column in `df` representing latitude coordinates of data points.
|
36
|
+
:type lat_col: str
|
37
|
+
:ivar lon_col: Name of the column in `df` representing longitude coordinates of data points.
|
38
|
+
:type lon_col: str
|
39
|
+
:ivar osm_map: Folium Map object that serves as a container for geographic visualization.
|
40
|
+
This is dynamically initialized based on the provided data.
|
41
|
+
:type osm_map: folium.Map
|
42
|
+
:ivar G: Subgraph of the provided `osmnx_graph`, extracted based on bounding box
|
43
|
+
derived from the input data.
|
44
|
+
:type G: networkx.classes.multidigraph.MultiDiGraph
|
45
|
+
:ivar map_html_title: HTML-escaped title for map visualization, shown on the interactive map.
|
46
|
+
:type map_html_title: str
|
47
|
+
:ivar zoom_start: Initial zoom level for the map when rendered.
|
48
|
+
:type zoom_start: int
|
49
|
+
:ivar fullscreen: Boolean option to enable or disable fullscreen control on the interactive map.
|
50
|
+
:type fullscreen: bool
|
51
|
+
:ivar fullscreen_position: Position of the fullscreen control on the map, e.g., 'topright'.
|
52
|
+
:type fullscreen_position: str
|
53
|
+
:ivar tiles: Default tile layer to be used for the map visualization.
|
54
|
+
:type tiles: str
|
55
|
+
:ivar verbose: Boolean flag indicating if verbose logging of operations should be enabled.
|
56
|
+
:type verbose: bool
|
57
|
+
:ivar sort_keys: Keys to sort the DataFrame by, if applicable, before processing.
|
58
|
+
:type sort_keys: list[str] or None
|
59
|
+
:ivar dt_field: Column name in `df` representing datetimes, if applicable, for specialized processing.
|
60
|
+
:type dt_field: str or None
|
61
|
+
:ivar dt: List of datetime objects extracted from `dt_field` in `df`, dynamically initialized.
|
62
|
+
:type dt: list[datetime.datetime] or None
|
63
|
+
:ivar calc_nearest_nodes: Boolean flag to indicate whether nearest nodes to the input
|
64
|
+
points should be calculated from `osmnx_graph`.
|
65
|
+
:type calc_nearest_nodes: bool
|
66
|
+
:ivar nearest_nodes: List of nearest node IDs for each point in the input data, if calculated.
|
67
|
+
:type nearest_nodes: list[int] or None
|
68
|
+
:ivar max_bounds: Boolean indicating whether bounds (default to Costa Rica) should be applied
|
69
|
+
to fit the map within those limits interactively.
|
70
|
+
:type max_bounds: bool
|
71
|
+
"""
|
14
72
|
tile_options = {
|
15
73
|
"OpenStreetMap": "OpenStreetMap",
|
16
74
|
"CartoDB": "cartodbpositron",
|
@@ -20,6 +78,47 @@ class BaseOsmMap:
|
|
20
78
|
bounds = [[8.0340, -85.9417], [11.2192, -82.5566]]
|
21
79
|
|
22
80
|
def __init__(self, osmnx_graph=None, df=None, **kwargs):
|
81
|
+
"""
|
82
|
+
Initializes a map visualization handler with input parameters for managing and displaying
|
83
|
+
spatial data in conjunction with an OSMnx graph. It validates input parameters for completeness
|
84
|
+
and correctness, and configures internal attributes for the visualization of map layers.
|
85
|
+
|
86
|
+
:param osmnx_graph: The OSMnx graph to be used for spatial data processing.
|
87
|
+
:param df: The pandas DataFrame containing spatial data.
|
88
|
+
:param kwargs: Additional keyword arguments for customization.
|
89
|
+
:type osmnx_graph: object
|
90
|
+
:type df: pandas.DataFrame
|
91
|
+
:type kwargs: dict
|
92
|
+
|
93
|
+
:raises ValueError: If `osmnx_graph` is not provided.
|
94
|
+
:raises ValueError: If `df` is not provided.
|
95
|
+
:raises ValueError: If the provided `df` is empty.
|
96
|
+
|
97
|
+
:attribute df: A copy of the provided DataFrame.
|
98
|
+
:attribute osmnx_graph: The input OSMnx graph used for spatial data operations.
|
99
|
+
:attribute lat_col: The name of the column in `df` containing latitude values.
|
100
|
+
:attribute lon_col: The name of the column in `df` containing longitude values.
|
101
|
+
:attribute osm_map: Internal representation of the map; initialized to None.
|
102
|
+
:attribute G: Placeholder for graph-related data; initialized to None.
|
103
|
+
:attribute map_html_title: Sanitized HTML title to be used in rendered map outputs.
|
104
|
+
:attribute zoom_start: The initial zoom level for the map visualization (default: 13).
|
105
|
+
:attribute fullscreen: Boolean flag to enable fullscreen map display (default: True).
|
106
|
+
:attribute fullscreen_position: The position of the fullscreen control button
|
107
|
+
(default: 'topright').
|
108
|
+
:attribute tiles: The tile set to be used for the map visualization (default: 'OpenStreetMap').
|
109
|
+
:attribute verbose: Boolean flag for enabling verbose output (default: False).
|
110
|
+
:attribute sort_keys: Optional parameter dictating sorting order for keys (default: None).
|
111
|
+
:attribute dt_field: Optional name of the datetime field in the DataFrame (default: None).
|
112
|
+
:attribute dt: Placeholder for date/time data; initialized to None.
|
113
|
+
:attribute calc_nearest_nodes: Boolean flag to calculate nearest nodes to coordinates
|
114
|
+
(default: False).
|
115
|
+
:attribute nearest_nodes: Placeholder for nearest nodes data; initialized to None.
|
116
|
+
:attribute max_bounds: Boolean flag to restrict the map view to maximum bounds
|
117
|
+
(default: False).
|
118
|
+
|
119
|
+
:note: This class also initializes necessary internal functionalities including DataFrame
|
120
|
+
pre-processing (`_prepare_df`) and base map setup (`_initialise_map`).
|
121
|
+
"""
|
23
122
|
if osmnx_graph is None:
|
24
123
|
raise ValueError('osmnx_graph must be provided')
|
25
124
|
if df is None:
|
@@ -50,6 +149,21 @@ class BaseOsmMap:
|
|
50
149
|
|
51
150
|
|
52
151
|
def _prepare_df(self):
|
152
|
+
"""
|
153
|
+
Prepares the underlying DataFrame for further processing.
|
154
|
+
|
155
|
+
This method performs several operations on the DataFrame, such as sorting,
|
156
|
+
resetting the index, and extracting relevant columns into various lists.
|
157
|
+
Additionally, it calculates the nearest nodes from an OSMnx graph if
|
158
|
+
required. The operations are governed by instance attributes and their
|
159
|
+
current states.
|
160
|
+
|
161
|
+
:raises AttributeError: If required attributes for processing are not
|
162
|
+
correctly set or do not exist in the DataFrame.
|
163
|
+
:param self: Object that contains the DataFrame (df) and other related
|
164
|
+
attributes required for processing.
|
165
|
+
:return: None
|
166
|
+
"""
|
53
167
|
if self.sort_keys:
|
54
168
|
self.df.sort_values(by=self.sort_keys, inplace=True)
|
55
169
|
self.df.reset_index(drop=True, inplace=True)
|
@@ -63,6 +177,15 @@ class BaseOsmMap:
|
|
63
177
|
|
64
178
|
|
65
179
|
def _initialise_map(self):
|
180
|
+
"""
|
181
|
+
Initializes an OpenStreetMap (OSM) map instance and extracts a subgraph of map
|
182
|
+
data based on provided GPS points. The map's central location is calculated
|
183
|
+
as the mean of the latitudinal and longitudinal values of the GPS points.
|
184
|
+
Additionally, a bounding box is determined to extract the relevant section
|
185
|
+
of the map graph.
|
186
|
+
|
187
|
+
:return: None
|
188
|
+
"""
|
66
189
|
gps_array = np.array(self.gps_points)
|
67
190
|
mean_latitude = np.mean(gps_array[:, 0])
|
68
191
|
mean_longitude = np.mean(gps_array[:, 1])
|
@@ -73,6 +196,24 @@ class BaseOsmMap:
|
|
73
196
|
|
74
197
|
|
75
198
|
def _attach_supported_tiles(self):
|
199
|
+
"""
|
200
|
+
Attach supported tile layers from the provided tile options to the map object.
|
201
|
+
|
202
|
+
This method normalizes the default tile layer name and ensures that it is not
|
203
|
+
duplicated in the map by filtering out the default tile from the `tile_options`.
|
204
|
+
Then, it iterates over the filtered tile options and attaches each supported
|
205
|
+
tile layer to the map object using the folium library.
|
206
|
+
|
207
|
+
:raises KeyError: If any key in `tile_options` is invalid or not found.
|
208
|
+
|
209
|
+
:param self:
|
210
|
+
The instance of the class. It is expected to have the following attributes:
|
211
|
+
- tiles (str): The name of the default tile layer.
|
212
|
+
- tile_options (Dict[str, str]): A dictionary of tile layer names
|
213
|
+
and their associated URLs.
|
214
|
+
- osm_map (folium.Map): The map object to which tile layers are attached.
|
215
|
+
:return: None
|
216
|
+
"""
|
76
217
|
# Normalize the default tile name to lowercase for comparison
|
77
218
|
normalized_default_tile = self.tiles.lower()
|
78
219
|
|
@@ -84,6 +225,22 @@ class BaseOsmMap:
|
|
84
225
|
|
85
226
|
|
86
227
|
def _get_bounding_box_from_points(self, margin=0.001):
|
228
|
+
"""
|
229
|
+
Calculate a bounding box from GPS points with an additional margin.
|
230
|
+
|
231
|
+
This method processes the GPS points in the `self.gps_points` list
|
232
|
+
and calculates a geographical bounding box encompassing all
|
233
|
+
points with an optional margin added to its boundaries. The
|
234
|
+
bounding box is defined by the northernmost, southernmost,
|
235
|
+
easternmost, and westernmost boundaries.
|
236
|
+
|
237
|
+
:param margin: A float value that adds a margin to the bounding
|
238
|
+
box boundaries.
|
239
|
+
Defaults to 0.001.
|
240
|
+
:return: A tuple containing four float values in the order:
|
241
|
+
north (northernmost boundary), south (southernmost boundary),
|
242
|
+
east (easternmost boundary), and west (westernmost boundary).
|
243
|
+
"""
|
87
244
|
latitudes = [point[0] for point in self.gps_points]
|
88
245
|
longitudes = [point[1] for point in self.gps_points]
|
89
246
|
|
@@ -96,6 +253,26 @@ class BaseOsmMap:
|
|
96
253
|
|
97
254
|
|
98
255
|
def _extract_subgraph(self, north, south, east, west):
|
256
|
+
"""
|
257
|
+
Extracts a subgraph from the OSMnx graph based on a specified bounding box.
|
258
|
+
|
259
|
+
This function takes the northern, southern, eastern, and western bounds to
|
260
|
+
define a geographic bounding box. It creates a polygon from these bounds and
|
261
|
+
identifies nodes within the graph that fall within this boundary. A new subgraph
|
262
|
+
is then created using these identified nodes. This functionality supports different
|
263
|
+
approaches depending on the OSMnx version.
|
264
|
+
|
265
|
+
:param north: The northern boundary of the bounding box.
|
266
|
+
:type north: float
|
267
|
+
:param south: The southern boundary of the bounding box.
|
268
|
+
:type south: float
|
269
|
+
:param east: The eastern boundary of the bounding box.
|
270
|
+
:type east: float
|
271
|
+
:param west: The western boundary of the bounding box.
|
272
|
+
:type west: float
|
273
|
+
:return: A subgraph extracted from the OSMnx graph containing nodes within the bounding box.
|
274
|
+
:rtype: networkx.Graph
|
275
|
+
"""
|
99
276
|
# Create a bounding box polygon
|
100
277
|
# from osmnx v2 this is how it is done
|
101
278
|
if ox.__version__ >= '2.0':
|
@@ -117,18 +294,45 @@ class BaseOsmMap:
|
|
117
294
|
|
118
295
|
@abstractmethod
|
119
296
|
def process_map(self):
|
297
|
+
"""
|
298
|
+
This abstract method serves as a blueprint for processing a map. It is intentionally
|
299
|
+
left unimplemented and must be overridden in any concrete subclass. Subclasses should
|
300
|
+
provide their own specific logic for map processing by implementing this interface.
|
301
|
+
|
302
|
+
:return: None
|
303
|
+
"""
|
120
304
|
# this is to be implemented at the subclass level
|
121
305
|
# implement here your specific map logic.
|
122
306
|
...
|
123
307
|
|
124
308
|
|
125
309
|
def pre_process_map(self):
|
310
|
+
"""
|
311
|
+
Pre-processes the map data.
|
312
|
+
|
313
|
+
This method is designed to be overridden in a subclass to perform specific
|
314
|
+
pre-processing procedures on map data. When overridden, the implementation
|
315
|
+
in the subclass must call `super().pre_process_map` first to inherit and
|
316
|
+
maintain the existing behavior of this base implementation.
|
317
|
+
|
318
|
+
:return: None
|
319
|
+
"""
|
126
320
|
# this is to be implemented at the subclass level
|
127
321
|
# call super().pre_process_map first to inherit the following behaviour
|
128
322
|
...
|
129
323
|
|
130
324
|
|
131
325
|
def _post_process_map(self):
|
326
|
+
"""
|
327
|
+
Performs post-processing tasks on the map object to enhance its functionality
|
328
|
+
and ensure required adjustments or attributes are attached to it. This method
|
329
|
+
is mainly used internally to finalize map setup based on current configurations.
|
330
|
+
|
331
|
+
:raises AttributeError: This error is raised if one of the required attributes
|
332
|
+
is not properly initialized or missing during the processing.
|
333
|
+
:raises ValueError: This error is raised if provided bounds are invalid or
|
334
|
+
incompatible for fitting.
|
335
|
+
"""
|
132
336
|
self._attach_supported_tiles()
|
133
337
|
self.add_tile_layer()
|
134
338
|
self._add_fullscreen()
|
@@ -138,26 +342,76 @@ class BaseOsmMap:
|
|
138
342
|
|
139
343
|
|
140
344
|
def add_tile_layer(self):
|
345
|
+
"""
|
346
|
+
Adds a tile layer to the OpenStreetMap (OSM) representation.
|
347
|
+
This method is intended to handle the inclusion of additional
|
348
|
+
tile layers in the map. Subclasses should override this method,
|
349
|
+
call their custom logic, and conclude with a call to this base
|
350
|
+
implementation to add necessary controls to the map instance.
|
351
|
+
|
352
|
+
:rtype: None
|
353
|
+
:return: This method does not return any value.
|
354
|
+
"""
|
141
355
|
# Override in subclass and call super().add_tile_layer at the end
|
142
356
|
folium.LayerControl().add_to(self.osm_map)
|
143
357
|
|
144
358
|
|
145
359
|
def _add_fullscreen(self):
|
360
|
+
"""
|
361
|
+
Adds a fullscreen functionality to the map if the fullscreen attribute is set.
|
362
|
+
|
363
|
+
This method checks whether the fullscreen mode is enabled by evaluating
|
364
|
+
the `fullscreen` attribute of the class. If `fullscreen` is set to True,
|
365
|
+
a Fullscreen instance is created with the specified position from
|
366
|
+
`fullscreen_position` and added to the map (`osm_map`).
|
367
|
+
|
368
|
+
:return: None
|
369
|
+
"""
|
146
370
|
if self.fullscreen:
|
147
371
|
Fullscreen(position=self.fullscreen_position).add_to(self.osm_map)
|
148
372
|
|
149
373
|
|
150
374
|
def _add_map_title(self):
|
375
|
+
"""
|
376
|
+
Adds a title to the map if the title is specified.
|
377
|
+
|
378
|
+
This method checks if the attribute `map_html_title` is set. If it is,
|
379
|
+
it creates an HTML element containing the title and adds it to the
|
380
|
+
map's root HTML structure using Folium's API.
|
381
|
+
|
382
|
+
:raises AttributeError: If `osm_map` or `osm_map.get_root()` is not defined
|
383
|
+
or does not support the necessary operations.
|
384
|
+
"""
|
151
385
|
if self.map_html_title:
|
152
386
|
self.osm_map.get_root().html.add_child(folium.Element(self.map_html_title))
|
153
387
|
|
154
388
|
|
155
389
|
@staticmethod
|
156
390
|
def _sanitize_html(input_html):
|
391
|
+
"""
|
392
|
+
Sanitizes the provided HTML input by escaping special HTML characters.
|
393
|
+
This method ensures the input string is safe for use in HTML contexts
|
394
|
+
by converting characters like `<`, `>`, and `&` into their corresponding
|
395
|
+
HTML-safe representations.
|
396
|
+
|
397
|
+
:param input_html: The HTML string that needs to be sanitized.
|
398
|
+
:type input_html: str
|
399
|
+
:return: The sanitized version of the input HTML string with special
|
400
|
+
characters escaped.
|
401
|
+
:rtype: str
|
402
|
+
"""
|
157
403
|
return html.escape(input_html)
|
158
404
|
|
159
405
|
|
160
406
|
def generate_map(self):
|
407
|
+
"""
|
408
|
+
Generates a map by executing preprocessing, main processing, and
|
409
|
+
post-processing tasks sequentially. This method combines multiple
|
410
|
+
stages to prepare and retrieve the final map object.
|
411
|
+
|
412
|
+
:return: The completed and processed OpenStreetMap map instance
|
413
|
+
:rtype: object
|
414
|
+
"""
|
161
415
|
self.pre_process_map()
|
162
416
|
self.process_map()
|
163
417
|
self._post_process_map()
|