cetk 0.0.4.dev0__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.
- cetk/__init__.py +87 -0
- cetk/db.py +41 -0
- cetk/edb/__init__.py +0 -0
- cetk/edb/apps.py +9 -0
- cetk/edb/cache.py +249 -0
- cetk/edb/const.py +26 -0
- cetk/edb/copy.py +18 -0
- cetk/edb/exporters.py +551 -0
- cetk/edb/importers/__init__.py +36 -0
- cetk/edb/importers/activity_import.py +279 -0
- cetk/edb/importers/codeset_import.py +124 -0
- cetk/edb/importers/gridsource_import.py +256 -0
- cetk/edb/importers/roadsource_import.py +1602 -0
- cetk/edb/importers/source_import.py +958 -0
- cetk/edb/importers/timevar_import.py +115 -0
- cetk/edb/importers/utils.py +92 -0
- cetk/edb/importers/validation.py +207 -0
- cetk/edb/ltreefield.py +76 -0
- cetk/edb/migrations/0001_initial.py +1731 -0
- cetk/edb/migrations/0002_data.py +156 -0
- cetk/edb/migrations/0003_create_gpkg_tables.py +14 -0
- cetk/edb/migrations/__init__.py +0 -0
- cetk/edb/models/__init__.py +49 -0
- cetk/edb/models/base.py +52 -0
- cetk/edb/models/common_models.py +144 -0
- cetk/edb/models/fleets.py +160 -0
- cetk/edb/models/gridsource_models.py +163 -0
- cetk/edb/models/road_classes.py +197 -0
- cetk/edb/models/road_models.py +413 -0
- cetk/edb/models/source_models.py +399 -0
- cetk/edb/models/timevar_models.py +111 -0
- cetk/edb/rasterize/__init__.py +1 -0
- cetk/edb/rasterize/rasterizer.py +1544 -0
- cetk/edb/signals.py +43 -0
- cetk/edb/traffic.py +75 -0
- cetk/edb/units.py +160 -0
- cetk/emissions/__init__.py +0 -0
- cetk/emissions/aggregate_emissions.sql +303 -0
- cetk/emissions/areasource_emissions.sql +53 -0
- cetk/emissions/calc.py +148 -0
- cetk/emissions/filters.py +179 -0
- cetk/emissions/gridsource_emissions.sql +52 -0
- cetk/emissions/pointsource_emissions.sql +53 -0
- cetk/emissions/queries.py +221 -0
- cetk/emissions/roadsource_emissions.sql +211 -0
- cetk/emissions/used_substances.sql +56 -0
- cetk/emissions/views.py +89 -0
- cetk/logging.py +68 -0
- cetk/settings.py +85 -0
- cetk/tools/__init__.py +1 -0
- cetk/tools/cetk_command.py +599 -0
- cetk/tools/manage.py +24 -0
- cetk/tools/utils.py +334 -0
- cetk/utils.py +62 -0
- cetk-0.0.4.dev0.dist-info/LICENSE +201 -0
- cetk-0.0.4.dev0.dist-info/METADATA +297 -0
- cetk-0.0.4.dev0.dist-info/RECORD +60 -0
- cetk-0.0.4.dev0.dist-info/WHEEL +5 -0
- cetk-0.0.4.dev0.dist-info/entry_points.txt +3 -0
- cetk-0.0.4.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1602 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from operator import itemgetter
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# need to import fiona before geopandas due to gpd bug causing circular imports.
|
|
9
|
+
import fiona # noqa
|
|
10
|
+
import geopandas as gpd
|
|
11
|
+
import numpy as np # noqa
|
|
12
|
+
import pandas as pd
|
|
13
|
+
from django.contrib.gis.gdal import CoordTransform # noqa
|
|
14
|
+
from django.contrib.gis.gdal import SpatialReference
|
|
15
|
+
from django.contrib.gis.gdal.geometries import LineString as GDALLineString
|
|
16
|
+
from django.contrib.gis.geos import Point, Polygon # noqa
|
|
17
|
+
from django.core.exceptions import ObjectDoesNotExist, ValidationError # noqa
|
|
18
|
+
from django.core.management.base import CommandError # noqa
|
|
19
|
+
from django.db import IntegrityError # noqa
|
|
20
|
+
from openpyxl import load_workbook
|
|
21
|
+
|
|
22
|
+
from cetk.edb.const import WGS84_SRID
|
|
23
|
+
from cetk.edb.models import ColdstartTimevar # noqa
|
|
24
|
+
from cetk.edb.models import (
|
|
25
|
+
CodeSet,
|
|
26
|
+
CongestionProfile,
|
|
27
|
+
Fleet,
|
|
28
|
+
FleetMemberFuel,
|
|
29
|
+
FlowTimevar,
|
|
30
|
+
PrefetchRoadClassAttributes,
|
|
31
|
+
RoadAttribute,
|
|
32
|
+
RoadClass,
|
|
33
|
+
RoadSource,
|
|
34
|
+
Substance,
|
|
35
|
+
TrafficSituation,
|
|
36
|
+
Vehicle,
|
|
37
|
+
VehicleEF,
|
|
38
|
+
VehicleFuel,
|
|
39
|
+
VehicleFuelComb,
|
|
40
|
+
get_valid_road_attribute_values,
|
|
41
|
+
)
|
|
42
|
+
from cetk.edb.units import emission_unit_to_si, vehicle_ef_unit_to_si # noqa
|
|
43
|
+
from cetk.utils import inbatch
|
|
44
|
+
|
|
45
|
+
from .timevar_import import import_timevarsheet
|
|
46
|
+
from .utils import import_error, worksheet_to_dataframe
|
|
47
|
+
|
|
48
|
+
log = logging.getLogger(__name__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
STATIC_ROAD_ATTRIBUTES = [
|
|
52
|
+
"aadt",
|
|
53
|
+
"nolanes",
|
|
54
|
+
"name",
|
|
55
|
+
"speed",
|
|
56
|
+
"width",
|
|
57
|
+
"median_strip_width",
|
|
58
|
+
"heavy_vehicle_share",
|
|
59
|
+
"slope",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def handle_msg(messages, msg, *, fail_early=False):
|
|
64
|
+
"""handle repeated error without bloating stderr/stdout.
|
|
65
|
+
|
|
66
|
+
args
|
|
67
|
+
messages: dict where messages are accumulated
|
|
68
|
+
msg: message string
|
|
69
|
+
fail_early: exit directly
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
if fail_early:
|
|
73
|
+
raise ImportError(msg)
|
|
74
|
+
|
|
75
|
+
if msg not in messages:
|
|
76
|
+
log.debug(f"debug: {msg}")
|
|
77
|
+
messages[msg] = 1
|
|
78
|
+
else:
|
|
79
|
+
messages[msg] += 1
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def filter_out(feature, exclude):
|
|
83
|
+
"""filter roads by attribute."""
|
|
84
|
+
|
|
85
|
+
for attr_name, val in exclude.items():
|
|
86
|
+
if isinstance(val, (str, int, float)):
|
|
87
|
+
if feature.get(attr_name) != val:
|
|
88
|
+
return False
|
|
89
|
+
elif str(feature.get(attr_name)) not in list(val):
|
|
90
|
+
return False
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def vehicles_excel_to_dict(file_path):
|
|
95
|
+
dtype_dict = {"name": str, "isheavy": bool, "info": str, "fuel": str}
|
|
96
|
+
for cs in CodeSet.objects.all():
|
|
97
|
+
dtype_dict["activitycode_" + cs.slug] = str
|
|
98
|
+
df = pd.read_excel(file_path, sheet_name="VehicleFuel", dtype=dtype_dict).fillna("")
|
|
99
|
+
|
|
100
|
+
# Extract the code sets from the DataFrame
|
|
101
|
+
activity_columns = [col for col in df.columns if col.startswith("activitycode_")]
|
|
102
|
+
code_sets = {
|
|
103
|
+
f"code_set{i+1}": col.split("_")[1] for i, col in enumerate(activity_columns)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
data = {**code_sets, "vehicles": []}
|
|
107
|
+
vehicles = []
|
|
108
|
+
for index, row in df.iterrows():
|
|
109
|
+
activitycodes = {}
|
|
110
|
+
for col in activity_columns:
|
|
111
|
+
ac_id = CodeSet.objects.get(slug=col.replace("activitycode_", "")).id
|
|
112
|
+
activitycodes[f"activitycode{ac_id}"] = row[col]
|
|
113
|
+
# Check if the vehicle already exists in the data dictionary
|
|
114
|
+
if row["name"] in vehicles:
|
|
115
|
+
vehicle = next(
|
|
116
|
+
(
|
|
117
|
+
vehicle
|
|
118
|
+
for vehicle in data["vehicles"]
|
|
119
|
+
if vehicle["name"] == row["name"]
|
|
120
|
+
),
|
|
121
|
+
None,
|
|
122
|
+
)
|
|
123
|
+
# vehicle exists, set fuel type
|
|
124
|
+
# first check that fuel type didn't exist yet
|
|
125
|
+
vehicle["fuels"][row["fuel"]] = activitycodes
|
|
126
|
+
else:
|
|
127
|
+
vehicle_data = {
|
|
128
|
+
"name": row["name"],
|
|
129
|
+
"isheavy": row["isheavy"],
|
|
130
|
+
"info": row["info"],
|
|
131
|
+
"fuels": {},
|
|
132
|
+
}
|
|
133
|
+
vehicle_data["fuels"][row["fuel"]] = activitycodes
|
|
134
|
+
data["vehicles"].append(vehicle_data)
|
|
135
|
+
vehicles.append(row["name"])
|
|
136
|
+
return data
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def roadclass_excel_to_dict(file_path, validation=False):
|
|
140
|
+
msg = []
|
|
141
|
+
df = pd.read_excel(file_path, sheet_name="RoadAttribute")
|
|
142
|
+
df_values = pd.read_excel(file_path, sheet_name="TrafficSituation", dtype=str)
|
|
143
|
+
data = {"attributes": []}
|
|
144
|
+
for index, row in df.iterrows():
|
|
145
|
+
try:
|
|
146
|
+
unique_values = list(set(df_values["attr:" + row["slug"]]))
|
|
147
|
+
data["attributes"].append(
|
|
148
|
+
{"name": row["name"], "slug": row["slug"], "values": unique_values}
|
|
149
|
+
)
|
|
150
|
+
except KeyError:
|
|
151
|
+
msg = import_error(
|
|
152
|
+
f"Attribute not found in TrafficSituation sheet: {row['slug']}",
|
|
153
|
+
validation=validation,
|
|
154
|
+
)
|
|
155
|
+
return data, msg
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def fleet_excel_to_dict(file_path, validation=False):
|
|
159
|
+
df = pd.read_excel(file_path, sheet_name="Fleet")
|
|
160
|
+
data = {}
|
|
161
|
+
fuel_columns = [col for col in df.columns if col.startswith("fuel:")]
|
|
162
|
+
msg = []
|
|
163
|
+
for index, row in df.iterrows():
|
|
164
|
+
fuels = {}
|
|
165
|
+
for i, col in enumerate(fuel_columns):
|
|
166
|
+
if row[col] > 0:
|
|
167
|
+
fuels[col[5:]] = row[col]
|
|
168
|
+
vehicle_data = {
|
|
169
|
+
"fraction": row["vehicle_fraction"],
|
|
170
|
+
"coldstart_fraction": row["coldstart_fraction"],
|
|
171
|
+
"timevar": row["flow_timevar"],
|
|
172
|
+
"coldstart_timevar": row["coldstart_timevar"],
|
|
173
|
+
"fuels": fuels,
|
|
174
|
+
}
|
|
175
|
+
if row["name"] in data.keys():
|
|
176
|
+
if row["vehicle"] in data[row["name"]]["vehicles"].keys():
|
|
177
|
+
msg = import_error(
|
|
178
|
+
f"duplicated vehicle {row['vehicle']} for fleet {row['name']}",
|
|
179
|
+
validation=validation,
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
data[row["name"]]["vehicles"][row["vehicle"]] = vehicle_data
|
|
183
|
+
else:
|
|
184
|
+
data[row["name"]] = {
|
|
185
|
+
"default_heavy_vehicle_share": row["default_heavy_vehicle_share"],
|
|
186
|
+
"vehicles": {row["vehicle"]: vehicle_data},
|
|
187
|
+
}
|
|
188
|
+
return data, msg
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def roadsource_excel_to_dict(file_path):
|
|
192
|
+
df = pd.read_excel(file_path, sheet_name="RoadSource")
|
|
193
|
+
attribute_columns = [col for col in df.columns if col.startswith("attr:")]
|
|
194
|
+
attributes = {col[5:]: str(df[col].values[0]) for col in attribute_columns}
|
|
195
|
+
tag_columns = [col for col in df.columns if col.startswith("tag:")]
|
|
196
|
+
tags = {col[4:]: str(df[col].values[0]) for col in tag_columns}
|
|
197
|
+
data = {
|
|
198
|
+
col: str(df[col].values[0])
|
|
199
|
+
for col in df.columns
|
|
200
|
+
if not (
|
|
201
|
+
col.startswith("attr:")
|
|
202
|
+
or col.startswith("tag:")
|
|
203
|
+
or pd.isnull(df[col].values[0])
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
data["roadclass"] = attributes
|
|
207
|
+
data["tags"] = tags
|
|
208
|
+
return data
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def import_traffic(filename, sheets, validation=False):
|
|
212
|
+
# in case the excel file is a big file, using workbook as argument instead
|
|
213
|
+
# of filename would be a good way to optimize import
|
|
214
|
+
workbook = load_workbook(filename=filename, data_only=True, read_only=True)
|
|
215
|
+
return_dict = {}
|
|
216
|
+
return_message = []
|
|
217
|
+
if "VehicleFuel" in sheets:
|
|
218
|
+
vehiclesettings = vehicles_excel_to_dict(filename)
|
|
219
|
+
elif "VehicleEmissionFactor" in sheets:
|
|
220
|
+
log.error(
|
|
221
|
+
"Cannot import or validate vehicle emfacs if VehicleFuel "
|
|
222
|
+
"is not set in the same file."
|
|
223
|
+
)
|
|
224
|
+
raise ImportError
|
|
225
|
+
if "VehicleEmissionFactor" in sheets:
|
|
226
|
+
df = worksheet_to_dataframe(workbook["VehicleEmissionFactor"].values)
|
|
227
|
+
unit = set(df["unit"])
|
|
228
|
+
if len(unit) > 1:
|
|
229
|
+
return_message.append(
|
|
230
|
+
import_error(
|
|
231
|
+
"Several units found for import " f"{unit}, can only use one",
|
|
232
|
+
validation=validation,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
unit = str(list(unit)[0])
|
|
236
|
+
updates = import_vehicles(
|
|
237
|
+
filename,
|
|
238
|
+
vehiclesettings,
|
|
239
|
+
unit=unit, # "kg/m"
|
|
240
|
+
encoding="utf-8",
|
|
241
|
+
overwrite=True,
|
|
242
|
+
validation=validation,
|
|
243
|
+
)
|
|
244
|
+
return_dict.update(updates)
|
|
245
|
+
if ("RoadAttribute" in sheets) and ("TrafficSituation" in sheets):
|
|
246
|
+
roadclass_settings, msg = roadclass_excel_to_dict(filename, validation)
|
|
247
|
+
return_message += msg
|
|
248
|
+
import_roadclasses(
|
|
249
|
+
filename,
|
|
250
|
+
roadclass_settings,
|
|
251
|
+
encoding="utf-8",
|
|
252
|
+
overwrite=True,
|
|
253
|
+
validation=validation,
|
|
254
|
+
)
|
|
255
|
+
elif ("RoadAttribute" in sheets) and ("TrafficSituation" in sheets):
|
|
256
|
+
raise ImportError(
|
|
257
|
+
"Have to import TrafficSituation and RoadAttribute simultaneously."
|
|
258
|
+
)
|
|
259
|
+
if "CongestionProfile" in sheets:
|
|
260
|
+
_, msg = import_congestionsheet(workbook, validation=validation)
|
|
261
|
+
return_message += msg
|
|
262
|
+
if "FlowTimevar" in sheets:
|
|
263
|
+
_, msg = import_timevarsheet(workbook, validation, sheetname="FlowTimevar")
|
|
264
|
+
return_message += msg
|
|
265
|
+
if "ColdstartTimevar" in sheets:
|
|
266
|
+
_, msg = import_timevarsheet(workbook, validation, sheetname="ColdstartTimevar")
|
|
267
|
+
return_message += msg
|
|
268
|
+
if "Fleet" in sheets:
|
|
269
|
+
try:
|
|
270
|
+
fleet_data, msg = fleet_excel_to_dict(filename, validation)
|
|
271
|
+
return_message += msg
|
|
272
|
+
_, msg = import_fleets(fleet_data, overwrite=True, validation=validation)
|
|
273
|
+
return_message += msg
|
|
274
|
+
except ImportError as err:
|
|
275
|
+
return {}, [f"{err}"]
|
|
276
|
+
if "RoadSource" in sheets:
|
|
277
|
+
try:
|
|
278
|
+
config = roadsource_excel_to_dict(filename)
|
|
279
|
+
roadfile_path = config["filepath"]
|
|
280
|
+
if not os.path.isabs(roadfile_path):
|
|
281
|
+
datadir = os.path.dirname(filename)
|
|
282
|
+
roadfile_path = os.path.join(datadir, roadfile_path)
|
|
283
|
+
updates, msg = import_roads(roadfile_path, config, validation=validation)
|
|
284
|
+
return_dict.update(updates)
|
|
285
|
+
return_message += msg
|
|
286
|
+
except ImportError as err:
|
|
287
|
+
return {}, [f"{err}"]
|
|
288
|
+
workbook.close()
|
|
289
|
+
return return_dict, return_message # update dict and messages
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def import_vehicles( # noqa: C901, PLR0912, PLR0915
|
|
293
|
+
vehicles_file,
|
|
294
|
+
config,
|
|
295
|
+
*,
|
|
296
|
+
only_ef=False,
|
|
297
|
+
overwrite=False,
|
|
298
|
+
validation=False,
|
|
299
|
+
unit="mg/km",
|
|
300
|
+
encoding="utf-8",
|
|
301
|
+
):
|
|
302
|
+
"""Import vehicles, fuels and traffic situations to ef-set.
|
|
303
|
+
|
|
304
|
+
args
|
|
305
|
+
vehicles_file: a csv-file or xlsx-file with vehicles and emission-factors
|
|
306
|
+
config: a dict with definitions of vehicle, fuels and codes
|
|
307
|
+
|
|
308
|
+
optional
|
|
309
|
+
only_ef: if True, only load emission factors - do not modify base-set tables
|
|
310
|
+
overwrite: if True, existing instances will be ovewritten
|
|
311
|
+
unit: unit of emission-factors to import, default is "mg/km"
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
# cache valid activity codes
|
|
315
|
+
valid_codes = OrderedDict()
|
|
316
|
+
code_sets = [None, None, None]
|
|
317
|
+
config = copy.deepcopy(config)
|
|
318
|
+
return_message = []
|
|
319
|
+
|
|
320
|
+
# cache activity-codes for code-sets specified in config file
|
|
321
|
+
for i in range(3):
|
|
322
|
+
code_nr = i + 1
|
|
323
|
+
code_sets[i] = config.pop(f"code_set{code_nr}", None)
|
|
324
|
+
|
|
325
|
+
if code_sets[i] is not None:
|
|
326
|
+
try:
|
|
327
|
+
code_sets[i] = CodeSet.objects.get(slug=code_sets[i])
|
|
328
|
+
except ObjectDoesNotExist:
|
|
329
|
+
return_message.append(
|
|
330
|
+
import_error(
|
|
331
|
+
f"Invalid codeset slug: {code_sets[i]}",
|
|
332
|
+
validation=validation,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
valid_codes[code_sets[i].slug] = {
|
|
337
|
+
ac.code: ac for ac in code_sets[i].codes.all()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
def validate_ac(code_data, valid_codes, validation=False, return_message=[]):
|
|
341
|
+
if "activitycode1" in code_data and len(valid_codes) == 0:
|
|
342
|
+
return_message.append(
|
|
343
|
+
import_error(
|
|
344
|
+
"no activity codes defined, but codes given for vehicle",
|
|
345
|
+
validation=validation,
|
|
346
|
+
)
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
for code_set_name, codes in valid_codes.items():
|
|
350
|
+
code_nr = CodeSet.objects.get(slug=code_set_name).id
|
|
351
|
+
try:
|
|
352
|
+
ac = code_data[f"activitycode{code_nr}"]
|
|
353
|
+
except KeyError:
|
|
354
|
+
return_message.append(
|
|
355
|
+
import_error(
|
|
356
|
+
f"no value specified for activity code {code_nr}"
|
|
357
|
+
f" ({code_set_name}) of vehicle/fuel combination "
|
|
358
|
+
f"'{vehicle_name}' - '{fuel_name}'",
|
|
359
|
+
validation=validation,
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
if ac not in codes and ac != "":
|
|
363
|
+
return_message.append(
|
|
364
|
+
import_error(
|
|
365
|
+
f"invalid value '{ac}' for activity code "
|
|
366
|
+
f"{code_nr} ({code_set_name})"
|
|
367
|
+
f" of vehicle/fuel combination "
|
|
368
|
+
f"'{vehicle_name}' - '{fuel_name}'",
|
|
369
|
+
validation=validation,
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
code_nr += 1
|
|
373
|
+
return return_message
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
mass_unit, length_unit = unit.split("/")
|
|
377
|
+
except ValueError:
|
|
378
|
+
log.error(f"invalid emission-factor unit {unit} specified in config-file")
|
|
379
|
+
raise
|
|
380
|
+
log.debug(f"emission factor units is: {unit}")
|
|
381
|
+
|
|
382
|
+
messages = {}
|
|
383
|
+
with Path(vehicles_file).open(encoding=encoding) as veh_file:
|
|
384
|
+
log.debug("reading emission-factor table")
|
|
385
|
+
dtype_dict = {
|
|
386
|
+
"vehicle": str,
|
|
387
|
+
"fuel": str,
|
|
388
|
+
"traffic_situation": str,
|
|
389
|
+
"substance": str,
|
|
390
|
+
"freeflow": float,
|
|
391
|
+
"heavy": float,
|
|
392
|
+
"saturated": float,
|
|
393
|
+
"stopngo": float,
|
|
394
|
+
"coldstart": float,
|
|
395
|
+
}
|
|
396
|
+
if Path(vehicles_file).suffix == ".csv":
|
|
397
|
+
df = pd.read_csv(
|
|
398
|
+
veh_file,
|
|
399
|
+
sep=";",
|
|
400
|
+
dtype=dtype_dict,
|
|
401
|
+
)
|
|
402
|
+
elif Path(vehicles_file).suffix == ".xlsx":
|
|
403
|
+
df = pd.read_excel(
|
|
404
|
+
vehicles_file, dtype=dtype_dict, sheet_name="VehicleEmissionFactor"
|
|
405
|
+
)
|
|
406
|
+
else:
|
|
407
|
+
raise ValueError(
|
|
408
|
+
"File for import vehicle emissions factors should be " + ".csv or .xlsx"
|
|
409
|
+
)
|
|
410
|
+
# indexes are set after reading csv in order to allow
|
|
411
|
+
# specification of dtypes for index columns
|
|
412
|
+
try:
|
|
413
|
+
df = df.set_index(["vehicle", "fuel", "traffic_situation", "substance"])
|
|
414
|
+
except KeyError as err:
|
|
415
|
+
raise ImportError(f"Invalid csv-file: {err}")
|
|
416
|
+
|
|
417
|
+
for col in ("freeflow", "heavy", "saturated", "stopngo", "coldstart"):
|
|
418
|
+
if col not in df.columns:
|
|
419
|
+
raise ImportError(
|
|
420
|
+
f"Required column '{col}' not found in file '{vehicles_file}"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
log.debug("checking that all substances exist in the database")
|
|
424
|
+
substances = {}
|
|
425
|
+
for subst in df.index.get_level_values(3).unique():
|
|
426
|
+
try:
|
|
427
|
+
substances[subst] = Substance.objects.get(slug=subst)
|
|
428
|
+
except ObjectDoesNotExist:
|
|
429
|
+
msg = f"substance {subst} does not exist in database"
|
|
430
|
+
return_message.append(import_error(msg), validation=validation)
|
|
431
|
+
|
|
432
|
+
# create vehicles
|
|
433
|
+
vehicle_defs = config.get("vehicles", [])
|
|
434
|
+
if len(vehicle_defs) > 0:
|
|
435
|
+
log.debug("processing vehicles")
|
|
436
|
+
df_vehicles = df.index.get_level_values(0).unique()
|
|
437
|
+
for veh in vehicle_defs:
|
|
438
|
+
veh_tmp = copy.deepcopy(veh)
|
|
439
|
+
fuel_defs = veh_tmp.pop("fuels", {})
|
|
440
|
+
try:
|
|
441
|
+
vehicle_name = veh_tmp.pop("name")
|
|
442
|
+
if overwrite and not only_ef:
|
|
443
|
+
(_, created,) = Vehicle.objects.update_or_create(
|
|
444
|
+
name=vehicle_name, defaults=veh_tmp
|
|
445
|
+
)
|
|
446
|
+
if created:
|
|
447
|
+
log.debug(f"created vehicle {vehicle_name}")
|
|
448
|
+
elif not only_ef:
|
|
449
|
+
try:
|
|
450
|
+
(_, created) = Vehicle.objects.get_or_create(
|
|
451
|
+
name=vehicle_name, defaults=veh_tmp
|
|
452
|
+
)
|
|
453
|
+
except IntegrityError:
|
|
454
|
+
return_message.append(
|
|
455
|
+
import_error(
|
|
456
|
+
"either duplicate specification or vehicle "
|
|
457
|
+
f"'{vehicle_name}' already exists.",
|
|
458
|
+
validation=validation,
|
|
459
|
+
)
|
|
460
|
+
)
|
|
461
|
+
if created:
|
|
462
|
+
log.debug(f"created vehicle {vehicle_name}")
|
|
463
|
+
elif not Vehicle.objects.filter(name=vehicle_name).exists():
|
|
464
|
+
return_message.append(
|
|
465
|
+
import_error( # noqa: TRY301
|
|
466
|
+
f"vehicle '{vehicle_name}' does not exist ",
|
|
467
|
+
validation=validation,
|
|
468
|
+
)
|
|
469
|
+
)
|
|
470
|
+
except Exception as err:
|
|
471
|
+
return_message.append(
|
|
472
|
+
import_error(
|
|
473
|
+
f"invalid specification of vehicle in config-file: {err}",
|
|
474
|
+
validation=validation,
|
|
475
|
+
)
|
|
476
|
+
)
|
|
477
|
+
if vehicle_name not in df_vehicles:
|
|
478
|
+
log.warning(
|
|
479
|
+
"warning: no emission-factors specified for vehicle"
|
|
480
|
+
f" '{vehicle_name}'",
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
vehicles = {veh.name: veh for veh in Vehicle.objects.all()}
|
|
484
|
+
|
|
485
|
+
for fuel_name, code_data in fuel_defs.items():
|
|
486
|
+
# fuel model only has a name, so overwrite/updating is not relevant
|
|
487
|
+
|
|
488
|
+
if not only_ef:
|
|
489
|
+
# allow modification of base-set tables
|
|
490
|
+
fuel, _ = VehicleFuel.objects.get_or_create(name=fuel_name)
|
|
491
|
+
else:
|
|
492
|
+
# only verify that fuel exist
|
|
493
|
+
try:
|
|
494
|
+
fuel = VehicleFuel.objects.get(name=fuel_name)
|
|
495
|
+
except ObjectDoesNotExist:
|
|
496
|
+
return_message.append(
|
|
497
|
+
import_error(
|
|
498
|
+
f"fuel {fuel_name} does not exist in database",
|
|
499
|
+
validation=validation,
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
return_message.append(
|
|
503
|
+
validate_ac(code_data, valid_codes, validation=validation)
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# get activity code model instances for each activity code
|
|
507
|
+
ac_codes = [None, None, None]
|
|
508
|
+
for i in range(3):
|
|
509
|
+
code_nr = i + 1
|
|
510
|
+
ac_codes[i] = code_data.get(f"activitycode{code_nr}", None)
|
|
511
|
+
if ac_codes[i] is not None and ac_codes[i] != "":
|
|
512
|
+
try:
|
|
513
|
+
ac_codes[i] = valid_codes[
|
|
514
|
+
CodeSet.objects.get(id=code_nr).slug
|
|
515
|
+
][ac_codes[i]]
|
|
516
|
+
except KeyError:
|
|
517
|
+
return_message.append(
|
|
518
|
+
import_error(
|
|
519
|
+
f"Found invalid activitycode {ac_codes[i]}",
|
|
520
|
+
validation=validation,
|
|
521
|
+
)
|
|
522
|
+
)
|
|
523
|
+
else:
|
|
524
|
+
ac_codes[i] = None
|
|
525
|
+
|
|
526
|
+
if overwrite and not only_ef:
|
|
527
|
+
_, created = VehicleFuelComb.objects.update_or_create(
|
|
528
|
+
vehicle=vehicles[vehicle_name],
|
|
529
|
+
fuel=fuel,
|
|
530
|
+
defaults={
|
|
531
|
+
"activitycode1": ac_codes[0],
|
|
532
|
+
"activitycode2": ac_codes[1],
|
|
533
|
+
"activitycode3": ac_codes[2],
|
|
534
|
+
},
|
|
535
|
+
)
|
|
536
|
+
if created:
|
|
537
|
+
log.debug(
|
|
538
|
+
"created vehicle-fuel combination "
|
|
539
|
+
f"'{vehicle_name}' - '{fuel_name}'"
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
elif not only_ef:
|
|
543
|
+
VehicleFuelComb.objects.get_or_create(
|
|
544
|
+
vehicle=vehicles[vehicle_name],
|
|
545
|
+
fuel=fuel,
|
|
546
|
+
defaults={
|
|
547
|
+
"activitycode1": ac_codes[0],
|
|
548
|
+
"activitycode2": ac_codes[1],
|
|
549
|
+
"activitycode3": ac_codes[2],
|
|
550
|
+
},
|
|
551
|
+
)
|
|
552
|
+
elif not VehicleFuelComb.objects.filter(
|
|
553
|
+
vehicle=vehicles[vehicle_name], fuel=fuel
|
|
554
|
+
).exists():
|
|
555
|
+
return_message.append(
|
|
556
|
+
import_error(
|
|
557
|
+
"vehicle/fuel combination "
|
|
558
|
+
f"'{vehicle_name}' - '{fuel_name}' "
|
|
559
|
+
"does not exist.",
|
|
560
|
+
validation=validation,
|
|
561
|
+
)
|
|
562
|
+
)
|
|
563
|
+
fuels = {fuel.name: fuel for fuel in VehicleFuel.objects.all()}
|
|
564
|
+
veh_fuel_combs = {
|
|
565
|
+
(comb.vehicle.name, comb.fuel.name)
|
|
566
|
+
for comb in VehicleFuelComb.objects.all().select_related("vehicle", "fuel")
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
# check that all combinations of vehicle/fuel in ef-table exist in db
|
|
570
|
+
for vehicle_name, fuel_name in {row[:2] for row in df.index}:
|
|
571
|
+
if not VehicleFuelComb.objects.filter(
|
|
572
|
+
fuel__name=fuel_name, vehicle__name=vehicle_name
|
|
573
|
+
).exists():
|
|
574
|
+
msg = (
|
|
575
|
+
f"emission-factors for undefined vehicle/fuel combination "
|
|
576
|
+
f"'{vehicle_name}' - '{fuel_name}' will not be loaded."
|
|
577
|
+
)
|
|
578
|
+
if msg not in messages:
|
|
579
|
+
messages[msg] = 1
|
|
580
|
+
else:
|
|
581
|
+
messages[msg] += 1
|
|
582
|
+
|
|
583
|
+
# create traffic situations
|
|
584
|
+
log.debug("creating traffic-situations")
|
|
585
|
+
existing_traffic_situations = {
|
|
586
|
+
ts.ts_id: ts for ts in TrafficSituation.objects.all()
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
# check if there are any new traffic-situations in ef table
|
|
590
|
+
traffic_situations = []
|
|
591
|
+
for ts_id in df.index.get_level_values(2).unique():
|
|
592
|
+
if ts_id not in existing_traffic_situations:
|
|
593
|
+
# traffic-situations are defined by ts_id only
|
|
594
|
+
# this means overwriting/updating is not relevant
|
|
595
|
+
if only_ef:
|
|
596
|
+
return_message.append(
|
|
597
|
+
import_error(
|
|
598
|
+
f"traffic-situation {ts_id} does not exist.",
|
|
599
|
+
validation=validation,
|
|
600
|
+
)
|
|
601
|
+
)
|
|
602
|
+
traffic_situations.append(TrafficSituation(ts_id=ts_id))
|
|
603
|
+
# create any new traffic-situations
|
|
604
|
+
if len(traffic_situations) > 0:
|
|
605
|
+
TrafficSituation.objects.bulk_create(traffic_situations)
|
|
606
|
+
|
|
607
|
+
# update look-up dict for traffic-situations
|
|
608
|
+
updated_traffic_situations = {ts.ts_id: ts for ts in TrafficSituation.objects.all()}
|
|
609
|
+
|
|
610
|
+
# create/update all vehicle emission factors
|
|
611
|
+
log.debug("creating/updating emission factors")
|
|
612
|
+
|
|
613
|
+
# get all pre-existing emission factors in ef-set
|
|
614
|
+
# store in dict with (veh, fuel, ts, subst) as keys
|
|
615
|
+
# and instance id as values
|
|
616
|
+
existing_ef_keys = {
|
|
617
|
+
vals[1:]: vals[0]
|
|
618
|
+
for vals in VehicleEF.objects.all().values_list(
|
|
619
|
+
"id",
|
|
620
|
+
"vehicle__name",
|
|
621
|
+
"fuel__name",
|
|
622
|
+
"traffic_situation__ts_id",
|
|
623
|
+
"substance__slug",
|
|
624
|
+
)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
efs_to_create = []
|
|
628
|
+
efs_to_update = []
|
|
629
|
+
for index, row in df.iterrows():
|
|
630
|
+
vehicle_name, fuel_name, ts_id, subst_slug = index
|
|
631
|
+
valid_ef = True
|
|
632
|
+
|
|
633
|
+
# check if ef already exists
|
|
634
|
+
key = (vehicle_name, fuel_name, ts_id, subst_slug)
|
|
635
|
+
traffic_situation = updated_traffic_situations[ts_id]
|
|
636
|
+
|
|
637
|
+
try:
|
|
638
|
+
vehicle = vehicles[vehicle_name]
|
|
639
|
+
except KeyError:
|
|
640
|
+
msg = f"undefined vehicle '{vehicle_name}' found in emission factor table"
|
|
641
|
+
valid_ef = False
|
|
642
|
+
|
|
643
|
+
try:
|
|
644
|
+
fuel = fuels[fuel_name]
|
|
645
|
+
except KeyError:
|
|
646
|
+
msg = f"undefined fuel '{fuel_name}' found in emission factor table"
|
|
647
|
+
valid_ef = False
|
|
648
|
+
|
|
649
|
+
if (vehicle_name, fuel_name) not in veh_fuel_combs:
|
|
650
|
+
msg = (
|
|
651
|
+
f"emission-factors for undefined vehicle/fuel combination "
|
|
652
|
+
f"'{vehicle_name}' - '{fuel_name}' will not be loaded."
|
|
653
|
+
)
|
|
654
|
+
valid_ef = False
|
|
655
|
+
|
|
656
|
+
if not valid_ef:
|
|
657
|
+
if msg not in messages:
|
|
658
|
+
messages[msg] = 1
|
|
659
|
+
else:
|
|
660
|
+
messages[msg] += 1
|
|
661
|
+
continue
|
|
662
|
+
|
|
663
|
+
substance = substances[subst_slug]
|
|
664
|
+
|
|
665
|
+
def get_ef(val):
|
|
666
|
+
if pd.isna(val):
|
|
667
|
+
return 0
|
|
668
|
+
return vehicle_ef_unit_to_si(val, mass_unit, length_unit)
|
|
669
|
+
|
|
670
|
+
if key in existing_ef_keys:
|
|
671
|
+
efs_to_update.append(
|
|
672
|
+
VehicleEF(
|
|
673
|
+
id=existing_ef_keys[key],
|
|
674
|
+
traffic_situation=traffic_situation,
|
|
675
|
+
substance=substance,
|
|
676
|
+
vehicle=vehicle,
|
|
677
|
+
fuel=fuel,
|
|
678
|
+
freeflow=get_ef(row.freeflow),
|
|
679
|
+
heavy=get_ef(row.heavy),
|
|
680
|
+
saturated=get_ef(row.saturated),
|
|
681
|
+
stopngo=get_ef(row.stopngo),
|
|
682
|
+
coldstart=get_ef(row.coldstart),
|
|
683
|
+
)
|
|
684
|
+
)
|
|
685
|
+
else:
|
|
686
|
+
efs_to_create.append(
|
|
687
|
+
VehicleEF(
|
|
688
|
+
traffic_situation=traffic_situation,
|
|
689
|
+
substance=substance,
|
|
690
|
+
vehicle=vehicle,
|
|
691
|
+
fuel=fuel,
|
|
692
|
+
freeflow=get_ef(row.freeflow),
|
|
693
|
+
heavy=get_ef(row.heavy),
|
|
694
|
+
saturated=get_ef(row.saturated),
|
|
695
|
+
stopngo=get_ef(row.stopngo),
|
|
696
|
+
coldstart=get_ef(row.coldstart),
|
|
697
|
+
)
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
if not overwrite and len(efs_to_update) > 0:
|
|
701
|
+
msg = "\n".join(
|
|
702
|
+
(
|
|
703
|
+
f"{ef.vehicle.name}, {ef.fuel.name}, "
|
|
704
|
+
f"{ef.traffic_situation.ts_id}, {ef.substance.slug}"
|
|
705
|
+
)
|
|
706
|
+
for ef in efs_to_update
|
|
707
|
+
)
|
|
708
|
+
return_message.append(
|
|
709
|
+
import_error(
|
|
710
|
+
f"The following emission factors already exist in the ef-set: {msg}",
|
|
711
|
+
validation=validation,
|
|
712
|
+
)
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
if len(efs_to_update) > 0:
|
|
716
|
+
VehicleEF.objects.bulk_update(
|
|
717
|
+
efs_to_update,
|
|
718
|
+
("freeflow", "heavy", "saturated", "stopngo", "coldstart"),
|
|
719
|
+
)
|
|
720
|
+
log.debug(f"updated {len(efs_to_update)} emission-factors")
|
|
721
|
+
return_dict = {"vehicle_emission_factors": {"updated": len(efs_to_update)}}
|
|
722
|
+
else:
|
|
723
|
+
return_dict = {"vehicle_emission_factors": {"updated": 0}}
|
|
724
|
+
for msg, nr in messages.items():
|
|
725
|
+
log.warning("warning: " + msg + f": {nr}")
|
|
726
|
+
|
|
727
|
+
if len(efs_to_create) > 0:
|
|
728
|
+
try:
|
|
729
|
+
VehicleEF.objects.bulk_create(efs_to_create)
|
|
730
|
+
log.debug(f"wrote {len(efs_to_create)} emission-factors")
|
|
731
|
+
return_dict["vehicle_emission_factors"]["created"] = len(efs_to_create)
|
|
732
|
+
except IntegrityError:
|
|
733
|
+
for ef in efs_to_create:
|
|
734
|
+
try:
|
|
735
|
+
ef.save()
|
|
736
|
+
except IntegrityError:
|
|
737
|
+
return_message.append(
|
|
738
|
+
import_error(
|
|
739
|
+
"duplicate emission-factors for: "
|
|
740
|
+
f"substance '{ef.substance.slug}, "
|
|
741
|
+
f"vehicle: '{ef.vehicle.name}', "
|
|
742
|
+
f"fuel: '{ef.fuel.name}', "
|
|
743
|
+
f"traffic-situation: '{ef.traffic_situation.ts_id}'",
|
|
744
|
+
validation=validation,
|
|
745
|
+
)
|
|
746
|
+
)
|
|
747
|
+
else:
|
|
748
|
+
return_dict["vehicle_emission_factors"]["created"] = 0
|
|
749
|
+
return return_dict
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
def import_roadclasses( # noqa: C901, PLR0912, PLR0915
|
|
753
|
+
roadclass_file, config, *, overwrite=False, validation=False, **kwargs
|
|
754
|
+
):
|
|
755
|
+
"""import roadclasses (traffic-situations must already exist in database)."""
|
|
756
|
+
return_message = []
|
|
757
|
+
encoding = kwargs.get("encoding", "utf-8")
|
|
758
|
+
try:
|
|
759
|
+
attributes = config["attributes"]
|
|
760
|
+
except KeyError:
|
|
761
|
+
ImportError("keyword 'attributes' not found in config")
|
|
762
|
+
|
|
763
|
+
log.debug("create road attributes")
|
|
764
|
+
|
|
765
|
+
# created objects are stored in a nested dict
|
|
766
|
+
defined_attributes = OrderedDict()
|
|
767
|
+
for ind, attr_dict in enumerate(attributes):
|
|
768
|
+
attr_dict_tmp = copy.deepcopy(attr_dict)
|
|
769
|
+
try:
|
|
770
|
+
values = attr_dict_tmp.pop("values")
|
|
771
|
+
except KeyError:
|
|
772
|
+
ImportError("keyword 'values' not found for in config")
|
|
773
|
+
|
|
774
|
+
try:
|
|
775
|
+
attr = RoadAttribute.objects.get(
|
|
776
|
+
name=attr_dict_tmp["name"], slug=attr_dict_tmp["slug"]
|
|
777
|
+
)
|
|
778
|
+
except RoadAttribute.DoesNotExist:
|
|
779
|
+
try:
|
|
780
|
+
attr = RoadAttribute.objects.create(
|
|
781
|
+
name=attr_dict_tmp["name"], slug=attr_dict_tmp["slug"], order=ind
|
|
782
|
+
)
|
|
783
|
+
except IntegrityError as err:
|
|
784
|
+
return_message.append(
|
|
785
|
+
import_error(
|
|
786
|
+
"invalid or duplicate road class attribute: "
|
|
787
|
+
f"{attr_dict['name']}: {err}",
|
|
788
|
+
validation=validation,
|
|
789
|
+
)
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
defined_attributes[attr] = {"attribute": attr}
|
|
793
|
+
for val in values:
|
|
794
|
+
defined_attributes[attr][val], _ = attr.values.get_or_create(value=val)
|
|
795
|
+
valid_attributes = get_valid_road_attribute_values()
|
|
796
|
+
for attr, values in valid_attributes.items():
|
|
797
|
+
if attr not in defined_attributes:
|
|
798
|
+
if overwrite:
|
|
799
|
+
attr.delete()
|
|
800
|
+
else:
|
|
801
|
+
return_message.append(
|
|
802
|
+
import_error(
|
|
803
|
+
"Unused road attribute '{attr.slug}' found",
|
|
804
|
+
validation=validation,
|
|
805
|
+
)
|
|
806
|
+
)
|
|
807
|
+
for label, value in values.items():
|
|
808
|
+
if label not in defined_attributes[attr]:
|
|
809
|
+
if overwrite:
|
|
810
|
+
value.delete()
|
|
811
|
+
else:
|
|
812
|
+
return_message.append(
|
|
813
|
+
import_error(
|
|
814
|
+
"Unused road attribute value '{label}' found",
|
|
815
|
+
validation=validation,
|
|
816
|
+
)
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
# cache all existing traffic situations and road-classes
|
|
820
|
+
traffic_situations = {ts.ts_id: ts for ts in TrafficSituation.objects.all()}
|
|
821
|
+
|
|
822
|
+
existing_roadclasses = {
|
|
823
|
+
tuple(rc.attributes.values()): rc.id
|
|
824
|
+
for rc in RoadClass.objects.prefetch_related(PrefetchRoadClassAttributes())
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
log.debug("reading roadclass table")
|
|
828
|
+
with Path(roadclass_file).open(encoding=encoding) as roadclass_stream:
|
|
829
|
+
roadclass_attributes = [a.slug for a in defined_attributes]
|
|
830
|
+
if Path(roadclass_file).suffix == ".csv":
|
|
831
|
+
try:
|
|
832
|
+
column_names = [*roadclass_attributes, "traffic_situation"]
|
|
833
|
+
df = pd.read_csv(
|
|
834
|
+
roadclass_stream, sep=";", dtype=str, usecols=column_names
|
|
835
|
+
).set_index([a.slug for a in defined_attributes])
|
|
836
|
+
except Exception as err:
|
|
837
|
+
ImportError(
|
|
838
|
+
"could not read csv, are all roadclass attributes "
|
|
839
|
+
f"{roadclass_attributes} and 'traffic_situation' "
|
|
840
|
+
f"given as columns? (error message: {err})"
|
|
841
|
+
)
|
|
842
|
+
elif Path(roadclass_file).suffix == ".xlsx":
|
|
843
|
+
try:
|
|
844
|
+
column_names = ["attr:" + rc for rc in roadclass_attributes]
|
|
845
|
+
column_names.append("traffic_situation")
|
|
846
|
+
df = pd.read_excel(
|
|
847
|
+
roadclass_file,
|
|
848
|
+
dtype=str,
|
|
849
|
+
sheet_name="TrafficSituation",
|
|
850
|
+
usecols=column_names,
|
|
851
|
+
)
|
|
852
|
+
df.rename(
|
|
853
|
+
columns={"attr:" + rc: rc for rc in roadclass_attributes},
|
|
854
|
+
inplace=True,
|
|
855
|
+
)
|
|
856
|
+
df.set_index([a.slug for a in defined_attributes], inplace=True)
|
|
857
|
+
except Exception as err:
|
|
858
|
+
raise ImportError(
|
|
859
|
+
"could not read xlsx, are all roadclass attributes "
|
|
860
|
+
f"{roadclass_attributes} and 'traffic_situation' "
|
|
861
|
+
"given as columns? and does the sheet TrafficSituation exist? "
|
|
862
|
+
f"(error message: {err})"
|
|
863
|
+
)
|
|
864
|
+
else:
|
|
865
|
+
raise ImportError(
|
|
866
|
+
"File for import vehicle emissions factors should be " + ".csv or .xlsx"
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
invalid_traffic_situations = []
|
|
870
|
+
roadclasses_to_create = []
|
|
871
|
+
roadclasses_to_update = []
|
|
872
|
+
row_nr = 0
|
|
873
|
+
for index, row in df.iterrows():
|
|
874
|
+
row_nr += 1
|
|
875
|
+
attribute_values = OrderedDict()
|
|
876
|
+
indexes = [index] if isinstance(index, str) else index
|
|
877
|
+
for attr, val in zip(defined_attributes, indexes):
|
|
878
|
+
if val not in defined_attributes[attr]:
|
|
879
|
+
return_message.append(
|
|
880
|
+
import_error(
|
|
881
|
+
f"Invalid value '{val}' for road attribute '{attr.slug}'",
|
|
882
|
+
validation=validation,
|
|
883
|
+
)
|
|
884
|
+
)
|
|
885
|
+
attribute_values[attr] = defined_attributes[attr][val]
|
|
886
|
+
|
|
887
|
+
try:
|
|
888
|
+
ts = traffic_situations[row.traffic_situation]
|
|
889
|
+
except KeyError:
|
|
890
|
+
invalid_traffic_situations.append(row.traffic_situation)
|
|
891
|
+
continue
|
|
892
|
+
rc = RoadClass(traffic_situation=ts)
|
|
893
|
+
try:
|
|
894
|
+
rc.id = existing_roadclasses[
|
|
895
|
+
tuple([v.value for v in attribute_values.values()])
|
|
896
|
+
]
|
|
897
|
+
roadclasses_to_update.append((rc, attribute_values.values()))
|
|
898
|
+
except KeyError:
|
|
899
|
+
roadclasses_to_create.append((rc, attribute_values.values()))
|
|
900
|
+
|
|
901
|
+
if len(invalid_traffic_situations) > 0:
|
|
902
|
+
return_message.append(
|
|
903
|
+
import_error(
|
|
904
|
+
"invalid traffic-situations:\n"
|
|
905
|
+
+ "\n ".join(invalid_traffic_situations),
|
|
906
|
+
validation=validation,
|
|
907
|
+
)
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
if len(roadclasses_to_update) > 0:
|
|
911
|
+
for rc, attribute_values in roadclasses_to_update:
|
|
912
|
+
if overwrite:
|
|
913
|
+
rc.save()
|
|
914
|
+
rc.attribute_values.set(attribute_values)
|
|
915
|
+
else:
|
|
916
|
+
return_message.append(
|
|
917
|
+
import_error(
|
|
918
|
+
f"roadclass '{rc}' already exists.", validation=validation
|
|
919
|
+
)
|
|
920
|
+
)
|
|
921
|
+
if len(roadclasses_to_create) > 0:
|
|
922
|
+
RoadClass.objects.bulk_create(map(itemgetter(0), roadclasses_to_create))
|
|
923
|
+
# list of saved roadclasses to avoid problem unsaved related attributes
|
|
924
|
+
roadclasses_to_create_saved = []
|
|
925
|
+
# created roadclasses should only be those who have no attribute values yet!
|
|
926
|
+
# this could go wrong if none of the roadclasses has attributes
|
|
927
|
+
# but then does not make sense to have several roadclasses anyhow.
|
|
928
|
+
created_roadclasses = RoadClass.objects.filter(attribute_values=None)
|
|
929
|
+
for i, rc in enumerate(created_roadclasses):
|
|
930
|
+
roadclasses_to_create_saved.append((rc, roadclasses_to_create[i][1]))
|
|
931
|
+
|
|
932
|
+
through_model = RoadClass.attribute_values.through
|
|
933
|
+
|
|
934
|
+
values = [
|
|
935
|
+
through_model(roadclass=rc, roadattributevalue=v)
|
|
936
|
+
for rc, vals in roadclasses_to_create_saved
|
|
937
|
+
for v in vals
|
|
938
|
+
]
|
|
939
|
+
|
|
940
|
+
through_model.objects.bulk_create(values)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
def import_congestionsheet(workbook, sheetname="CongestionProfile", validation=False):
|
|
944
|
+
congestion_data = workbook[sheetname].values
|
|
945
|
+
df_congestion = worksheet_to_dataframe(congestion_data)
|
|
946
|
+
congestion_dict = {}
|
|
947
|
+
# NB this only works if Excel file has exact same format
|
|
948
|
+
nr_profiles = (len(df_congestion["ID"]) + 1) // 25
|
|
949
|
+
for i in range(nr_profiles):
|
|
950
|
+
label = df_congestion["ID"][i * 25]
|
|
951
|
+
typeday = np.asarray(
|
|
952
|
+
df_congestion[
|
|
953
|
+
[
|
|
954
|
+
"monday",
|
|
955
|
+
"tuesday",
|
|
956
|
+
"wednesday",
|
|
957
|
+
"thursday",
|
|
958
|
+
"friday",
|
|
959
|
+
"saturday",
|
|
960
|
+
"sunday",
|
|
961
|
+
]
|
|
962
|
+
][i * 25 : i * 25 + 24]
|
|
963
|
+
)
|
|
964
|
+
congestion_dict[label] = {"traffic_condition": typeday}
|
|
965
|
+
return_dict, return_message = import_congestion_profiles(
|
|
966
|
+
congestion_dict, overwrite=True, validation=validation
|
|
967
|
+
)
|
|
968
|
+
return_dict = {"congestion": {"updated or created": len(congestion_dict)}}
|
|
969
|
+
return return_dict, return_message
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def import_congestion_profiles(profile_data, *, overwrite=False, validation=False):
|
|
973
|
+
"""import congestion profiles."""
|
|
974
|
+
return_message = []
|
|
975
|
+
# Profile instances must not be created by bulk_create as the save function
|
|
976
|
+
# is overloaded to calculate the normation constant.
|
|
977
|
+
|
|
978
|
+
def make_profiles(data):
|
|
979
|
+
retdict = {}
|
|
980
|
+
for name, timevar_data in data.items():
|
|
981
|
+
try:
|
|
982
|
+
traffic_condition = timevar_data["traffic_condition"]
|
|
983
|
+
if type(traffic_condition) == list:
|
|
984
|
+
traffic_condition = str(traffic_condition)
|
|
985
|
+
else:
|
|
986
|
+
traffic_condition = np.array2string(
|
|
987
|
+
traffic_condition, separator=","
|
|
988
|
+
).replace("\n", "")
|
|
989
|
+
if overwrite:
|
|
990
|
+
newobj = CongestionProfile.objects.update_or_create(
|
|
991
|
+
name=name,
|
|
992
|
+
defaults={"traffic_condition": traffic_condition},
|
|
993
|
+
)
|
|
994
|
+
else:
|
|
995
|
+
try:
|
|
996
|
+
newobj = CongestionProfile.objects.create(
|
|
997
|
+
name=name,
|
|
998
|
+
traffic_condition=traffic_condition,
|
|
999
|
+
)
|
|
1000
|
+
except IntegrityError:
|
|
1001
|
+
raise IntegrityError(
|
|
1002
|
+
f"Congestion-profile {name} " f"already exist in inventory "
|
|
1003
|
+
)
|
|
1004
|
+
retdict[name] = newobj
|
|
1005
|
+
except KeyError:
|
|
1006
|
+
return_message.append(
|
|
1007
|
+
import_error(
|
|
1008
|
+
f"Invalid specification of congestion-profile {name}"
|
|
1009
|
+
f", is 'traffic_condition' specified?",
|
|
1010
|
+
validation=validation,
|
|
1011
|
+
)
|
|
1012
|
+
)
|
|
1013
|
+
return retdict, return_message
|
|
1014
|
+
|
|
1015
|
+
profiles = {}
|
|
1016
|
+
profiles["profiles"], return_message = make_profiles(profile_data)
|
|
1017
|
+
return profiles, return_message
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
def import_fleets(
|
|
1021
|
+
data, *, overwrite=False, validation=False
|
|
1022
|
+
): # noqa: C901, PLR0912, PLR0915
|
|
1023
|
+
"""import fleets
|
|
1024
|
+
|
|
1025
|
+
args
|
|
1026
|
+
data: a dict with fleets
|
|
1027
|
+
|
|
1028
|
+
optional
|
|
1029
|
+
overwrite: True means existing instances will be overwritten
|
|
1030
|
+
|
|
1031
|
+
"""
|
|
1032
|
+
|
|
1033
|
+
existing_fuels = {fuel.name: fuel for fuel in VehicleFuel.objects.all()}
|
|
1034
|
+
existing_vehicles = {vehicle.name: vehicle for vehicle in Vehicle.objects.all()}
|
|
1035
|
+
existing_flow_timevars = {tvar.name: tvar for tvar in FlowTimevar.objects.all()}
|
|
1036
|
+
existing_coldstart_timevars = {
|
|
1037
|
+
tvar.name: tvar for tvar in ColdstartTimevar.objects.all()
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
fleets = {}
|
|
1041
|
+
return_message = []
|
|
1042
|
+
for name, fleet_data in data.items():
|
|
1043
|
+
fleet_data_tmp = copy.deepcopy(fleet_data)
|
|
1044
|
+
try:
|
|
1045
|
+
members_data = fleet_data_tmp.pop("vehicles", [])
|
|
1046
|
+
default_heavy_vehicle_share = fleet_data_tmp["default_heavy_vehicle_share"]
|
|
1047
|
+
except KeyError:
|
|
1048
|
+
return_message.append(
|
|
1049
|
+
import_error(
|
|
1050
|
+
f"no 'default_heavy_vehicle_share' specified for fleet '{name}'",
|
|
1051
|
+
validation=validation,
|
|
1052
|
+
)
|
|
1053
|
+
)
|
|
1054
|
+
if overwrite:
|
|
1055
|
+
fleets[name], _ = Fleet.objects.update_or_create(
|
|
1056
|
+
name=name,
|
|
1057
|
+
defaults={
|
|
1058
|
+
"default_heavy_vehicle_share": default_heavy_vehicle_share,
|
|
1059
|
+
},
|
|
1060
|
+
)
|
|
1061
|
+
else:
|
|
1062
|
+
try:
|
|
1063
|
+
fleets[name] = Fleet.objects.create(
|
|
1064
|
+
name=name,
|
|
1065
|
+
default_heavy_vehicle_share=default_heavy_vehicle_share,
|
|
1066
|
+
)
|
|
1067
|
+
except IntegrityError:
|
|
1068
|
+
return_message.append(
|
|
1069
|
+
import_error(
|
|
1070
|
+
f"either duplicate specification in file or "
|
|
1071
|
+
f"fleet '{name}' already exist in inventory",
|
|
1072
|
+
validation=validation,
|
|
1073
|
+
)
|
|
1074
|
+
)
|
|
1075
|
+
|
|
1076
|
+
members = OrderedDict()
|
|
1077
|
+
heavy_member_sum = 0
|
|
1078
|
+
light_member_sum = 0
|
|
1079
|
+
for vehicle_name, member_data in members_data.items():
|
|
1080
|
+
fuels_data = member_data.pop("fuels", [])
|
|
1081
|
+
timevar_name = member_data.pop("timevar")
|
|
1082
|
+
coldstart_timevar_name = member_data.pop("coldstart_timevar")
|
|
1083
|
+
|
|
1084
|
+
try:
|
|
1085
|
+
veh = existing_vehicles[vehicle_name]
|
|
1086
|
+
except KeyError:
|
|
1087
|
+
return_message.append(
|
|
1088
|
+
import_error(
|
|
1089
|
+
f"Vehicle {vehicle_name} specified in sheet Fleet "
|
|
1090
|
+
"does not exist in sheet VehicleFuel.",
|
|
1091
|
+
validation=validation,
|
|
1092
|
+
)
|
|
1093
|
+
)
|
|
1094
|
+
continue
|
|
1095
|
+
# accumulate member fractions to ensure they sum up to 1.0
|
|
1096
|
+
if veh.isheavy:
|
|
1097
|
+
heavy_member_sum += member_data["fraction"]
|
|
1098
|
+
else:
|
|
1099
|
+
light_member_sum += member_data["fraction"]
|
|
1100
|
+
|
|
1101
|
+
if timevar_name is not None:
|
|
1102
|
+
try:
|
|
1103
|
+
member_data["timevar"] = existing_flow_timevars[timevar_name]
|
|
1104
|
+
except KeyError:
|
|
1105
|
+
return_message.append(
|
|
1106
|
+
import_error(
|
|
1107
|
+
f"timevar '{timevar_name}' specified for vehicle "
|
|
1108
|
+
f"'{vehicle_name}' in fleet '{name}' does not "
|
|
1109
|
+
f"exist in inventory",
|
|
1110
|
+
validation=validation,
|
|
1111
|
+
)
|
|
1112
|
+
)
|
|
1113
|
+
else:
|
|
1114
|
+
member_data["timevar"] = None
|
|
1115
|
+
|
|
1116
|
+
if coldstart_timevar_name is not None:
|
|
1117
|
+
try:
|
|
1118
|
+
member_data["coldstart_timevar"] = existing_coldstart_timevars[
|
|
1119
|
+
coldstart_timevar_name
|
|
1120
|
+
]
|
|
1121
|
+
except KeyError:
|
|
1122
|
+
return_message.append(
|
|
1123
|
+
import_error(
|
|
1124
|
+
f"coldstart timevar '{coldstart_timevar_name}' "
|
|
1125
|
+
f"specified for vehicle '{vehicle_name}' "
|
|
1126
|
+
f"in fleet '{name}' does not exist in inventory",
|
|
1127
|
+
validation=validation,
|
|
1128
|
+
)
|
|
1129
|
+
)
|
|
1130
|
+
else:
|
|
1131
|
+
member_data["coldstart_timevar"] = None
|
|
1132
|
+
|
|
1133
|
+
try:
|
|
1134
|
+
if overwrite:
|
|
1135
|
+
members[vehicle_name], new = fleets[name].vehicles.update_or_create(
|
|
1136
|
+
vehicle=existing_vehicles[vehicle_name], defaults=member_data
|
|
1137
|
+
)
|
|
1138
|
+
# if updating fleet member, remove any old fleet member fuels
|
|
1139
|
+
# this allows overwriting with fewer fuels than before
|
|
1140
|
+
# and avoids remaining obsolete member fuels
|
|
1141
|
+
if not new:
|
|
1142
|
+
members[vehicle_name].fuels.all().delete()
|
|
1143
|
+
else:
|
|
1144
|
+
try:
|
|
1145
|
+
members[vehicle_name] = fleets[name].vehicles.create(
|
|
1146
|
+
vehicle=existing_vehicles[vehicle_name], **member_data
|
|
1147
|
+
)
|
|
1148
|
+
except IntegrityError:
|
|
1149
|
+
return_message.append(
|
|
1150
|
+
import_error(
|
|
1151
|
+
"Either duplicate specification of fleet member in"
|
|
1152
|
+
"config, or fleetmember already exist in inventory",
|
|
1153
|
+
validation=validation,
|
|
1154
|
+
)
|
|
1155
|
+
)
|
|
1156
|
+
except (KeyError, TypeError):
|
|
1157
|
+
return_message.append(
|
|
1158
|
+
import_error(
|
|
1159
|
+
f"invalid specification of vehicle '{vehicle_name}' in "
|
|
1160
|
+
f"fleet '{name}', must specify 'timevar','coldstart_timevar', "
|
|
1161
|
+
"fraction, 'coldstart_fraction' and 'fuels'",
|
|
1162
|
+
validation=validation,
|
|
1163
|
+
)
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
fuel_sum = 0
|
|
1167
|
+
member_fuels = []
|
|
1168
|
+
if not isinstance(fuels_data, dict):
|
|
1169
|
+
return_message.append(
|
|
1170
|
+
import_error(
|
|
1171
|
+
f"invalid specification of fuels for '{vehicle_name}'"
|
|
1172
|
+
f"in fleet '{name}', fuels should be specified as:\n"
|
|
1173
|
+
f"fuels:\n fuel1: 0.4\n fuel2: 0.6",
|
|
1174
|
+
validation=validation,
|
|
1175
|
+
)
|
|
1176
|
+
)
|
|
1177
|
+
for fuel_name, fuel_fraction in fuels_data.items():
|
|
1178
|
+
fuel_sum += fuel_fraction
|
|
1179
|
+
member_fuels.append(
|
|
1180
|
+
FleetMemberFuel(
|
|
1181
|
+
fuel=existing_fuels[fuel_name],
|
|
1182
|
+
fleet_member=members[vehicle_name],
|
|
1183
|
+
fraction=fuel_fraction,
|
|
1184
|
+
)
|
|
1185
|
+
)
|
|
1186
|
+
if fuel_sum != 1.0:
|
|
1187
|
+
return_message.append(
|
|
1188
|
+
import_error(
|
|
1189
|
+
f"sum of fuel fractions does not sum up to 1.0 (sum={fuel_sum})"
|
|
1190
|
+
f" for '{vehicle_name}s' of fleet '{name}' in inventory ",
|
|
1191
|
+
validation=validation,
|
|
1192
|
+
)
|
|
1193
|
+
)
|
|
1194
|
+
FleetMemberFuel.objects.bulk_create(member_fuels)
|
|
1195
|
+
if heavy_member_sum > 0 and abs(heavy_member_sum - 1.0) >= 0.005:
|
|
1196
|
+
return_message.append(
|
|
1197
|
+
import_error(
|
|
1198
|
+
f"sum of heavy fleet members does not sum up to 1.0 "
|
|
1199
|
+
f"(sum={heavy_member_sum}) "
|
|
1200
|
+
f"for fleet '{name}' in inventory ",
|
|
1201
|
+
validation=validation,
|
|
1202
|
+
)
|
|
1203
|
+
)
|
|
1204
|
+
if light_member_sum > 0 and abs(light_member_sum - 1.0) >= 0.005:
|
|
1205
|
+
return_message.append(
|
|
1206
|
+
import_error(
|
|
1207
|
+
f"sum of light fleet members does not sum up to 1.0 "
|
|
1208
|
+
f"(sum={light_member_sum}) "
|
|
1209
|
+
f"for fleet '{name}' in inventory ",
|
|
1210
|
+
validation=validation,
|
|
1211
|
+
)
|
|
1212
|
+
)
|
|
1213
|
+
return len(fleets), return_message
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
def import_roads( # noqa: C901, PLR0912, PLR0915
|
|
1217
|
+
roadfile,
|
|
1218
|
+
config,
|
|
1219
|
+
exclude=None,
|
|
1220
|
+
only=None,
|
|
1221
|
+
chunksize=1000,
|
|
1222
|
+
progress_callback=None,
|
|
1223
|
+
validation=False,
|
|
1224
|
+
):
|
|
1225
|
+
"""Import a road network."""
|
|
1226
|
+
return_message = []
|
|
1227
|
+
datasource = gpd.read_file(roadfile)
|
|
1228
|
+
|
|
1229
|
+
if "srid" in config:
|
|
1230
|
+
src_proj = SpatialReference(config["srid"])
|
|
1231
|
+
else:
|
|
1232
|
+
src_proj = SpatialReference(datasource.crs.to_epsg())
|
|
1233
|
+
target_proj = SpatialReference(WGS84_SRID)
|
|
1234
|
+
trans = CoordTransform(src_proj, target_proj)
|
|
1235
|
+
|
|
1236
|
+
# get attribute mappings from road input file to road-source fields
|
|
1237
|
+
# get dict of static attributes to read from road file
|
|
1238
|
+
attr_dict = {
|
|
1239
|
+
attr: config.get(attr) for attr in STATIC_ROAD_ATTRIBUTES if attr in config
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
defaults = config.pop("defaults", {})
|
|
1243
|
+
|
|
1244
|
+
if "fleet" in config:
|
|
1245
|
+
# prefetch fleets and store in dict for quick lookups
|
|
1246
|
+
fleets = {fleet.name: fleet for fleet in Fleet.objects.all()}
|
|
1247
|
+
else:
|
|
1248
|
+
fleet_name = defaults.get("fleet", "default")
|
|
1249
|
+
default_fleet, created = Fleet.objects.get_or_create(
|
|
1250
|
+
name=fleet_name, defaults={"default_heavy_vehicle_share": 0.05}
|
|
1251
|
+
)
|
|
1252
|
+
if created:
|
|
1253
|
+
log.warning(
|
|
1254
|
+
"no entry for 'fleet' in road import config file"
|
|
1255
|
+
", assigning an empty default fleet for all imported roads"
|
|
1256
|
+
)
|
|
1257
|
+
else:
|
|
1258
|
+
log.warning(
|
|
1259
|
+
f"assigning default fleet '{fleet_name}' for all imported roads"
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
# prefetch congestion profiles and store in dict for quick lookups
|
|
1263
|
+
congestion_profiles = {prof.name: prof for prof in CongestionProfile.objects.all()}
|
|
1264
|
+
default_congestion_profile_name = defaults.get("congestion_profile")
|
|
1265
|
+
default_congestion_profile = None
|
|
1266
|
+
if config.get("congestion_profile") is None:
|
|
1267
|
+
if default_congestion_profile_name is None:
|
|
1268
|
+
log.warning(
|
|
1269
|
+
"no field specified for 'congestion_profile' in road import config"
|
|
1270
|
+
" and no default specified"
|
|
1271
|
+
", no congestion profile will be specified for imported roads"
|
|
1272
|
+
)
|
|
1273
|
+
else:
|
|
1274
|
+
log.warning(
|
|
1275
|
+
"no field specified for 'congestion_profile' in road import config"
|
|
1276
|
+
f", default congestion profile '{default_congestion_profile_name}' "
|
|
1277
|
+
"will be used for all imported roads"
|
|
1278
|
+
)
|
|
1279
|
+
try:
|
|
1280
|
+
default_congestion_profile = congestion_profiles[
|
|
1281
|
+
default_congestion_profile_name
|
|
1282
|
+
]
|
|
1283
|
+
except KeyError:
|
|
1284
|
+
return_message.append(
|
|
1285
|
+
import_error(
|
|
1286
|
+
"default congestin profile "
|
|
1287
|
+
f"'{default_congestion_profile_name}' does not exist",
|
|
1288
|
+
validation=validation,
|
|
1289
|
+
)
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
valid_values = get_valid_road_attribute_values()
|
|
1293
|
+
# get valid roadclass attributes
|
|
1294
|
+
if "roadclass" in defaults:
|
|
1295
|
+
default_roadclass_attributes = {}
|
|
1296
|
+
for attr, values in valid_values.items():
|
|
1297
|
+
value = defaults["roadclass"][attr.slug]
|
|
1298
|
+
if attr.slug not in defaults["roadclass"]:
|
|
1299
|
+
return_message.append(
|
|
1300
|
+
import_error(
|
|
1301
|
+
f"incomplete default roadclass attribute mappings,"
|
|
1302
|
+
f" missing '{attr.slug}'",
|
|
1303
|
+
validation=validation,
|
|
1304
|
+
)
|
|
1305
|
+
)
|
|
1306
|
+
if value not in values:
|
|
1307
|
+
return_message.append(
|
|
1308
|
+
import_error(
|
|
1309
|
+
f"invalid roadclass attribute value {value} for"
|
|
1310
|
+
f" attribute {attr.slug}",
|
|
1311
|
+
validation=validation,
|
|
1312
|
+
)
|
|
1313
|
+
)
|
|
1314
|
+
default_roadclass_attributes[attr.slug] = value
|
|
1315
|
+
else:
|
|
1316
|
+
default_roadclass_attributes = {
|
|
1317
|
+
a.slug: a.values.first().value for a in valid_values
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
def generate_key(attribute_values, defined_attributes):
|
|
1321
|
+
return tuple([attribute_values[attr.slug] for attr in defined_attributes])
|
|
1322
|
+
|
|
1323
|
+
roadclasses = {
|
|
1324
|
+
generate_key(rc.attributes, valid_values): rc
|
|
1325
|
+
for rc in RoadClass.objects.prefetch_related(
|
|
1326
|
+
PrefetchRoadClassAttributes()
|
|
1327
|
+
).all()
|
|
1328
|
+
}
|
|
1329
|
+
if "roadclass" in config:
|
|
1330
|
+
# get attribute mappings for roadclass attributes
|
|
1331
|
+
roadclass_attr_dict = config["roadclass"]
|
|
1332
|
+
# prefetch roadclasses and store in dict for quick lookups
|
|
1333
|
+
for attr in valid_values:
|
|
1334
|
+
if attr.slug not in roadclass_attr_dict:
|
|
1335
|
+
return_message.append(
|
|
1336
|
+
import_error(
|
|
1337
|
+
"incomplete roadclass attribute mappings,"
|
|
1338
|
+
+ f" missing '{attr.slug}'",
|
|
1339
|
+
validation=validation,
|
|
1340
|
+
)
|
|
1341
|
+
)
|
|
1342
|
+
else:
|
|
1343
|
+
log.warning(
|
|
1344
|
+
"no entry 'roadclass' found in config "
|
|
1345
|
+
"for road import, assigning a default roadclass to all imported roads"
|
|
1346
|
+
)
|
|
1347
|
+
roadclass_attr_dict = None
|
|
1348
|
+
|
|
1349
|
+
# if no mappings are specified for roadclass attributes,
|
|
1350
|
+
# a default roadclass and traffic situation will be created
|
|
1351
|
+
|
|
1352
|
+
default_ts, created = TrafficSituation.objects.get_or_create(ts_id="default")
|
|
1353
|
+
if created:
|
|
1354
|
+
log.warning(
|
|
1355
|
+
"a 'default' traffic situation is created fo the default roadclass"
|
|
1356
|
+
)
|
|
1357
|
+
try:
|
|
1358
|
+
default_roadclass = roadclasses[
|
|
1359
|
+
generate_key(default_roadclass_attributes, valid_values)
|
|
1360
|
+
]
|
|
1361
|
+
except KeyError:
|
|
1362
|
+
default_roadclass = RoadClass.objects.create_from_attributes(
|
|
1363
|
+
default_roadclass_attributes, traffic_situation=default_ts
|
|
1364
|
+
)
|
|
1365
|
+
|
|
1366
|
+
# get dict of tags to read from road file
|
|
1367
|
+
tags_dict = config.get("tags", None)
|
|
1368
|
+
tag_defaults = defaults.pop("tags", {})
|
|
1369
|
+
messages = {}
|
|
1370
|
+
|
|
1371
|
+
def make_road(feature): # noqa: C901, PLR0912, PLR0915
|
|
1372
|
+
source_geom = feature.geometry
|
|
1373
|
+
|
|
1374
|
+
if np.shape(source_geom.xy)[1] < 2:
|
|
1375
|
+
msg = "invalid geometry (< 2 nodes), instance not imported"
|
|
1376
|
+
handle_msg(messages, msg)
|
|
1377
|
+
raise ValidationError(msg)
|
|
1378
|
+
|
|
1379
|
+
gdalgeom = GDALLineString(source_geom.wkt)
|
|
1380
|
+
gdalgeom.coord_dim = 2
|
|
1381
|
+
gdalgeom.transform(trans)
|
|
1382
|
+
geom = gdalgeom.geos
|
|
1383
|
+
road_data = {"geom": geom}
|
|
1384
|
+
|
|
1385
|
+
for target_name, source_name in attr_dict.items():
|
|
1386
|
+
default_value = defaults.get(target_name, None)
|
|
1387
|
+
if source_name is not None:
|
|
1388
|
+
try:
|
|
1389
|
+
val = feature.get(source_name)
|
|
1390
|
+
except UnicodeDecodeError:
|
|
1391
|
+
return_message.append(
|
|
1392
|
+
import_error(
|
|
1393
|
+
f"could not decode string in field {source_name}, "
|
|
1394
|
+
"only encoding utf-8 is supported",
|
|
1395
|
+
validation=validation,
|
|
1396
|
+
)
|
|
1397
|
+
)
|
|
1398
|
+
except KeyError:
|
|
1399
|
+
return_message.append(
|
|
1400
|
+
import_error(
|
|
1401
|
+
f"No field named '{source_name}' found in "
|
|
1402
|
+
f"input file '{roadfile}'",
|
|
1403
|
+
validation=validation,
|
|
1404
|
+
)
|
|
1405
|
+
)
|
|
1406
|
+
if pd.isnull(val):
|
|
1407
|
+
val = default_value
|
|
1408
|
+
else:
|
|
1409
|
+
msg = f"no source field specified for target field {target_name}"
|
|
1410
|
+
if default_value is not None:
|
|
1411
|
+
msg += f", using default value '{default_value}'"
|
|
1412
|
+
else:
|
|
1413
|
+
msg += " and no default value specified"
|
|
1414
|
+
handle_msg(messages, msg)
|
|
1415
|
+
val = default_value
|
|
1416
|
+
|
|
1417
|
+
if val is None:
|
|
1418
|
+
handle_msg(messages, f"field {target_name} of road is None")
|
|
1419
|
+
if target_name == "name":
|
|
1420
|
+
road_data[target_name] = ""
|
|
1421
|
+
else:
|
|
1422
|
+
road_data[target_name] = val
|
|
1423
|
+
|
|
1424
|
+
if "width" in road_data and (
|
|
1425
|
+
road_data["width"] == 0 or road_data["width"] == ""
|
|
1426
|
+
):
|
|
1427
|
+
road_data["width"] = RoadSource._meta.get_field("width").default
|
|
1428
|
+
handle_msg(
|
|
1429
|
+
messages,
|
|
1430
|
+
"invalid value (0m) for road width, "
|
|
1431
|
+
f"using default value {road_data['width']}m",
|
|
1432
|
+
)
|
|
1433
|
+
|
|
1434
|
+
if (
|
|
1435
|
+
"width" in road_data
|
|
1436
|
+
and type(road_data["width"]) == str
|
|
1437
|
+
and ("m" in road_data["width"])
|
|
1438
|
+
):
|
|
1439
|
+
try:
|
|
1440
|
+
road_data["width"] = float(road_data["width"].replace("m", ""))
|
|
1441
|
+
except ValueError:
|
|
1442
|
+
road_data["width"] = RoadSource._meta.get_field("width").default
|
|
1443
|
+
handle_msg(
|
|
1444
|
+
messages,
|
|
1445
|
+
"invalid value (including unit m) for road width, "
|
|
1446
|
+
f"using default value {road_data['width']}m",
|
|
1447
|
+
)
|
|
1448
|
+
|
|
1449
|
+
road = RoadSource(**road_data)
|
|
1450
|
+
|
|
1451
|
+
if "roadclass" in config:
|
|
1452
|
+
try:
|
|
1453
|
+
rc_key = tuple(
|
|
1454
|
+
[
|
|
1455
|
+
str(feature.get(roadclass_attr_dict[attr.slug])) or "-"
|
|
1456
|
+
for attr in valid_values
|
|
1457
|
+
]
|
|
1458
|
+
)
|
|
1459
|
+
except (ValueError, IndexError):
|
|
1460
|
+
return_message.append(
|
|
1461
|
+
import_error(
|
|
1462
|
+
f"No field named '{roadclass_attr_dict[attr.slug]}' found in "
|
|
1463
|
+
f"input file '{roadfile}'",
|
|
1464
|
+
validation=validation,
|
|
1465
|
+
)
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
try:
|
|
1469
|
+
road.roadclass = roadclasses[rc_key]
|
|
1470
|
+
except KeyError:
|
|
1471
|
+
handle_msg(
|
|
1472
|
+
messages,
|
|
1473
|
+
f"no roadclass with attribute values {rc_key} in inventory ",
|
|
1474
|
+
fail_early=True,
|
|
1475
|
+
)
|
|
1476
|
+
raise ValidationError(msg)
|
|
1477
|
+
else:
|
|
1478
|
+
road.roadclass = default_roadclass
|
|
1479
|
+
|
|
1480
|
+
if "congestion_profile" in config:
|
|
1481
|
+
try:
|
|
1482
|
+
field_name = config["congestion_profile"]
|
|
1483
|
+
name = feature.get(field_name) if field_name is not None else None
|
|
1484
|
+
except ValueError:
|
|
1485
|
+
return_message.append(
|
|
1486
|
+
import_error(
|
|
1487
|
+
f"No field named '{field_name}' found "
|
|
1488
|
+
f"in input file '{roadfile}'",
|
|
1489
|
+
validation=validation,
|
|
1490
|
+
)
|
|
1491
|
+
)
|
|
1492
|
+
except KeyError:
|
|
1493
|
+
return_message.append(
|
|
1494
|
+
import_error(
|
|
1495
|
+
f"No field named '{field_name}' found "
|
|
1496
|
+
f"in input file '{roadfile}'",
|
|
1497
|
+
validation=validation,
|
|
1498
|
+
)
|
|
1499
|
+
)
|
|
1500
|
+
if name is None:
|
|
1501
|
+
road.congestion_profile = default_congestion_profile
|
|
1502
|
+
elif name not in congestion_profiles:
|
|
1503
|
+
handle_msg(
|
|
1504
|
+
messages,
|
|
1505
|
+
f"no congestion profile with name '{name}' in inventory ",
|
|
1506
|
+
fail_early=True,
|
|
1507
|
+
)
|
|
1508
|
+
raise ValidationError(msg)
|
|
1509
|
+
else:
|
|
1510
|
+
road.congestion_profile = congestion_profiles[name]
|
|
1511
|
+
else:
|
|
1512
|
+
road.congestion_profile = default_congestion_profile
|
|
1513
|
+
|
|
1514
|
+
if "fleet" in config:
|
|
1515
|
+
try:
|
|
1516
|
+
field_name = config["fleet"]
|
|
1517
|
+
name = feature.get(field_name)
|
|
1518
|
+
except (KeyError, IndexError):
|
|
1519
|
+
return_message.append(
|
|
1520
|
+
import_error(
|
|
1521
|
+
f"No field named '{field_name}' found "
|
|
1522
|
+
f"in input file '{roadfile}'",
|
|
1523
|
+
validation=validation,
|
|
1524
|
+
)
|
|
1525
|
+
)
|
|
1526
|
+
|
|
1527
|
+
if name is None or name not in fleets:
|
|
1528
|
+
handle_msg(
|
|
1529
|
+
messages,
|
|
1530
|
+
f"no fleet with name '{name}' in inventory ",
|
|
1531
|
+
fail_early=True,
|
|
1532
|
+
)
|
|
1533
|
+
raise ValidationError(msg)
|
|
1534
|
+
road.fleet = fleets[name]
|
|
1535
|
+
else:
|
|
1536
|
+
road.fleet = default_fleet
|
|
1537
|
+
|
|
1538
|
+
if tags_dict is not None:
|
|
1539
|
+
tag_data = {}
|
|
1540
|
+
for tag_key, source_name in tags_dict.items():
|
|
1541
|
+
tag_default = tag_defaults.get(tag_key, None)
|
|
1542
|
+
|
|
1543
|
+
if source_name is not None:
|
|
1544
|
+
try:
|
|
1545
|
+
val = feature.get(source_name)
|
|
1546
|
+
except KeyError:
|
|
1547
|
+
return_message.append(
|
|
1548
|
+
import_error(
|
|
1549
|
+
f"No field named '{source_name}' found in "
|
|
1550
|
+
f"input file '{roadfile}'",
|
|
1551
|
+
validation=validation,
|
|
1552
|
+
)
|
|
1553
|
+
)
|
|
1554
|
+
else:
|
|
1555
|
+
val = None
|
|
1556
|
+
|
|
1557
|
+
if val is not None:
|
|
1558
|
+
tag_data[tag_key] = val
|
|
1559
|
+
elif tag_default is not None:
|
|
1560
|
+
tag_data[tag_key] = tag_default
|
|
1561
|
+
handle_msg(messages, f"road lack a value for tag '{tag_key}'")
|
|
1562
|
+
road.tags = tag_data
|
|
1563
|
+
return road
|
|
1564
|
+
|
|
1565
|
+
roads = []
|
|
1566
|
+
count = 0
|
|
1567
|
+
nroads = len(datasource)
|
|
1568
|
+
old_progress = -1
|
|
1569
|
+
ncreated = 0
|
|
1570
|
+
for features in inbatch(datasource.iterrows(), chunksize):
|
|
1571
|
+
for feature in features:
|
|
1572
|
+
count += 1
|
|
1573
|
+
|
|
1574
|
+
progress = count / nroads * 100
|
|
1575
|
+
if int(progress) > old_progress:
|
|
1576
|
+
if not validation:
|
|
1577
|
+
log.debug(f"done {int(progress)}%")
|
|
1578
|
+
old_progress = int(progress)
|
|
1579
|
+
|
|
1580
|
+
if exclude is not None and filter_out(feature[1], exclude):
|
|
1581
|
+
continue
|
|
1582
|
+
if only is not None and not filter_out(feature[1], only):
|
|
1583
|
+
continue
|
|
1584
|
+
|
|
1585
|
+
try:
|
|
1586
|
+
road = make_road(feature[1])
|
|
1587
|
+
except ValidationError:
|
|
1588
|
+
continue
|
|
1589
|
+
roads.append(road)
|
|
1590
|
+
RoadSource.objects.bulk_create(roads)
|
|
1591
|
+
ncreated += len(roads)
|
|
1592
|
+
roads = []
|
|
1593
|
+
if progress_callback:
|
|
1594
|
+
progress_callback(count)
|
|
1595
|
+
log.debug(f"created {ncreated} roads")
|
|
1596
|
+
return_dict = {"roads": {"created": ncreated, "updated": 0}}
|
|
1597
|
+
if len(messages) > 0:
|
|
1598
|
+
log.warning("Summary: ")
|
|
1599
|
+
for msg, nr in messages.items():
|
|
1600
|
+
log.warning("- " + msg + f": {nr} roads")
|
|
1601
|
+
|
|
1602
|
+
return return_dict, return_message
|