hkayabilisim commited on
Commit
c721171
·
1 Parent(s): 07fcfdd

Added GeoTIFF support and utilities page

Browse files
pyproject.toml CHANGED
@@ -18,7 +18,8 @@ dependencies = [
18
  "scipy",
19
  "pandas",
20
  "networkx",
21
- "openpyxl"
 
22
  ]
23
 
24
  [tool.hatch.version]
 
18
  "scipy",
19
  "pandas",
20
  "networkx",
21
+ "openpyxl",
22
+ "rasterio"
23
  ]
24
 
25
  [tool.hatch.version]
requirements.txt CHANGED
@@ -6,4 +6,5 @@ lorem_text
6
  matplotlib
7
  psycopg2-binary
8
  scipy
9
- pandas
 
 
6
  matplotlib
7
  psycopg2-binary
8
  scipy
9
+ pandas
10
+ rasterio
tomorrowcities/content/articles/data_formats.md CHANGED
@@ -49,5 +49,8 @@ where
49
  * **residents (integer)** stores the number of individual live in the building.
50
  * **expStr (string)**
51
 
 
 
 
52
 
53
 
 
49
  * **residents (integer)** stores the number of individual live in the building.
50
  * **expStr (string)**
51
 
52
+ ### Intensity Measures
53
+ Whether it is flood, debris or earthquake, every hazard map should contain at least two properties: a point geometry and intensity measure denotes by 'im'. The data can be provided via GeoTIFF or GeoJSON format. TIFF files should contain CRS
54
+ information so that the engine could map the coordinates to a common CRS to conduct calculations.
55
 
56
 
tomorrowcities/content/articles/welcome.md CHANGED
@@ -15,6 +15,13 @@ category:
15
  TCDSE is a web application designed to conduct computational tasks to generate information needed for decision mechanisms in designing future cities. The web application, which will be referred as TCDSE for short, contains a computational engine capable of executing several hazard scenarios on different exposure datasets and infrastructures.
16
 
17
  ## What is New?
 
 
 
 
 
 
 
18
  * Info box is added next to the map to see overall information and building/landuse details
19
  when clicked.
20
  * Implementation Capacity Score is added. If medium or low is selected, then building-level metrics is increased by 25% and 50%, respectively. If high is selected, there is no change in the metrics.
 
15
  TCDSE is a web application designed to conduct computational tasks to generate information needed for decision mechanisms in designing future cities. The web application, which will be referred as TCDSE for short, contains a computational engine capable of executing several hazard scenarios on different exposure datasets and infrastructures.
16
 
17
  ## What is New?
18
+ * basemap is changed to ESri.WorldImagery to see the landscapes especially rivers.
19
+ * utilities page is added.
20
+ * Excel to GeoJSON converted is added to utilities page.
21
+ * GeoTIFF support is added.
22
+ * When an intensity layer is added via GeoTIFF format, only the non-zero intensity measure are retained by the engine.
23
+ * In map visualization of intensity layer, only the largest 500k points are displayed to render the map faster.
24
+ * rasterio.transform.xy function is replaced with a faster local implementation.
25
  * Info box is added next to the map to see overall information and building/landuse details
26
  when clicked.
27
  * Implementation Capacity Score is added. If medium or low is selected, then building-level metrics is increased by 25% and 50%, respectively. If high is selected, there is no change in the metrics.
tomorrowcities/pages/__init__.py CHANGED
@@ -5,7 +5,7 @@ import dataclasses
5
 
6
  from ..data import articles
7
 
8
- route_order = ["/", "docs","engine","settings","account"]
9
 
10
  def check_auth(route, children):
11
  # This can be replaced by a custom function that checks if the user is
@@ -13,7 +13,7 @@ def check_auth(route, children):
13
 
14
  # routes that are public or only for admin
15
  # the rest only requires login
16
- public_paths = ["/","docs","engine","account"]
17
  admin_paths = ["settings"]
18
 
19
 
 
5
 
6
  from ..data import articles
7
 
8
+ route_order = ["/", "docs","engine","utilities","settings","account"]
9
 
10
  def check_auth(route, children):
11
  # This can be replaced by a custom function that checks if the user is
 
13
 
14
  # routes that are public or only for admin
15
  # the rest only requires login
16
+ public_paths = ["/","docs","engine","utilities","account"]
17
  admin_paths = ["settings"]
18
 
19
 
tomorrowcities/pages/engine.py CHANGED
@@ -10,6 +10,9 @@ from typing import Tuple, Optional
10
  import ipyleaflet
11
  from ipyleaflet import AwesomeIcon, Marker
12
  import numpy as np
 
 
 
13
  import logging, sys
14
  #logging.basicConfig(stream=sys.stderr, level=logging.INFO)
15
 
@@ -96,9 +99,7 @@ layers = solara.reactive({
96
  'force_render': solara.reactive(False),
97
  'visible': solara.reactive(False),
98
  'extra_cols': {'ds': 0, 'is_damaged': False, 'is_operational': True},
99
- 'cols_required': set(['geometry', 'fltytype', 'strctype', 'utilfcltyc', 'indpnode', 'guid',
100
- 'node_id', 'x_coord', 'y_coord', 'pwr_plant', 'serv_area', 'n_bldgs',
101
- 'income', 'eq_vuln']),
102
  'cols': set(['geometry', 'fltytype', 'strctype', 'utilfcltyc', 'indpnode', 'guid',
103
  'node_id', 'x_coord', 'y_coord', 'pwr_plant', 'serv_area', 'n_bldgs',
104
  'income', 'eq_vuln'])},
@@ -110,8 +111,7 @@ layers = solara.reactive({
110
  'force_render': solara.reactive(False),
111
  'visible': solara.reactive(False),
112
  'extra_cols': {},
113
- 'cols_required': set(['from_node', 'direction', 'pipetype', 'edge_id', 'guid', 'capacity',
114
- 'geometry', 'to_node', 'length']),
115
  'cols': set(['from_node', 'direction', 'pipetype', 'edge_id', 'guid', 'capacity',
116
  'geometry', 'to_node', 'length'])},
117
  'power fragility': {
@@ -261,7 +261,9 @@ def create_map_layer(df, name):
261
  return existing_map_layer
262
 
263
  if name == "intensity":
264
- locs = np.array([df.geometry.y.to_list(), df.geometry.x.to_list(), df.im.to_list()]).transpose().tolist()
 
 
265
  map_layer = ipyleaflet.Heatmap(locations=locs, radius = 10)
266
  elif name == "landuse":
267
  map_layer = ipyleaflet.GeoJSON(data = json.loads(df.to_json()),
@@ -282,7 +284,7 @@ def create_map_layer(df, name):
282
  x = node.geometry.x
283
  y = node.geometry.y
284
  marker_color = 'blue' if node['is_operational'] else 'red'
285
- icon_name = 'fa-industry' if node['pwr_plant'] else 'bolt'
286
  icon_color = 'black'
287
  marker = Marker(icon=AwesomeIcon(
288
  name=icon_name,
@@ -302,6 +304,52 @@ def create_map_layer(df, name):
302
  layers.value['layers'][name]['force_render'].set(False)
303
  return map_layer
304
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  @solara.component
306
  def MetricWidget(name, description, value, max_value, render_count):
307
  value, set_value = solara.use_state_or_update(value)
@@ -342,6 +390,8 @@ def import_data(fileinfo: solara.components.file_drop.FileInfo):
342
  extension = fileinfo['name'].split('.')[-1]
343
  if extension == 'xlsx':
344
  df = pd.read_excel(data)
 
 
345
  else:
346
  json_string = data.decode('utf-8')
347
  json_data = json.loads(json_string)
@@ -491,7 +541,7 @@ def MapViewer():
491
  zoom, set_zoom = solara.use_state(default_zoom)
492
  #center, set_center = solara.use_state(default_center)
493
 
494
- base_map = ipyleaflet.basemaps["Stamen"]["Watercolor"]
495
  base_layer = ipyleaflet.TileLayer.element(url=base_map.build_url())
496
  map_layers = [base_layer]
497
 
 
10
  import ipyleaflet
11
  from ipyleaflet import AwesomeIcon, Marker
12
  import numpy as np
13
+ import rasterio
14
+ from rasterio.warp import calculate_default_transform, reproject, Resampling
15
+ import io
16
  import logging, sys
17
  #logging.basicConfig(stream=sys.stderr, level=logging.INFO)
18
 
 
99
  'force_render': solara.reactive(False),
100
  'visible': solara.reactive(False),
101
  'extra_cols': {'ds': 0, 'is_damaged': False, 'is_operational': True},
102
+ 'cols_required': set(['geometry', 'node_id', 'pwr_plant', 'n_bldgs', 'eq_vuln']),
 
 
103
  'cols': set(['geometry', 'fltytype', 'strctype', 'utilfcltyc', 'indpnode', 'guid',
104
  'node_id', 'x_coord', 'y_coord', 'pwr_plant', 'serv_area', 'n_bldgs',
105
  'income', 'eq_vuln'])},
 
111
  'force_render': solara.reactive(False),
112
  'visible': solara.reactive(False),
113
  'extra_cols': {},
114
+ 'cols_required': set(['geometry','from_node','to_node', 'edge_id']),
 
115
  'cols': set(['from_node', 'direction', 'pipetype', 'edge_id', 'guid', 'capacity',
116
  'geometry', 'to_node', 'length'])},
117
  'power fragility': {
 
261
  return existing_map_layer
262
 
263
  if name == "intensity":
264
+ # Take the largest 500_000 values to display
265
+ df_limited = df.sort_values(by='im',ascending=False).head(500_000)
266
+ locs = np.array([df_limited.geometry.y.to_list(), df_limited.geometry.x.to_list(), df_limited.im.to_list()]).transpose().tolist()
267
  map_layer = ipyleaflet.Heatmap(locations=locs, radius = 10)
268
  elif name == "landuse":
269
  map_layer = ipyleaflet.GeoJSON(data = json.loads(df.to_json()),
 
284
  x = node.geometry.x
285
  y = node.geometry.y
286
  marker_color = 'blue' if node['is_operational'] else 'red'
287
+ icon_name = 'fa-industry' if node['pwr_plant'] == 1 else 'bolt'
288
  icon_color = 'black'
289
  marker = Marker(icon=AwesomeIcon(
290
  name=icon_name,
 
304
  layers.value['layers'][name]['force_render'].set(False)
305
  return map_layer
306
 
307
+ def fast_transform_xy(T,x,y):
308
+ TI = rasterio.transform.IDENTITY.translation(0.5, 0.5)
309
+ TI_mat = np.array([[TI[0],TI[1],TI[2]],[TI[3],TI[4],TI[5]]])
310
+ T_mat = np.array([[T[0],T[1],T[2]],[T[3],T[4],T[5]]])
311
+ n = len(x)
312
+ first_input = np.ones((3,n))
313
+ first_input[0,:] = x
314
+ first_input[1,:] = y
315
+ first_pass = np.dot(TI_mat, first_input)
316
+ second_inp = np.concatenate([first_pass[[1]],first_pass[[0]],first_input[[2]]])
317
+ second_pass = np.dot(T_mat, second_inp)
318
+ return second_pass[0], second_pass[1]
319
+
320
+ def read_tiff(file_bytes):
321
+ byte_io = io.BytesIO(file_bytes)
322
+ with rasterio.open(byte_io) as src:
323
+ ims = src.read()
324
+
325
+ current_crs = src.crs
326
+ target_crs = 'EPSG:4326'
327
+ transform, width, height = calculate_default_transform(current_crs, target_crs, src.width, src.height, *src.bounds)
328
+ ims_transformed = np.zeros((height, width))
329
+
330
+ print('start reproject ..........')
331
+ reproject(
332
+ source=ims[0],
333
+ destination=ims_transformed,
334
+ src_transform=src.transform,
335
+ src_crs=current_crs,
336
+ dst_transform=transform,
337
+ dst_crs=target_crs,
338
+ resampling=Resampling.nearest)
339
+
340
+
341
+ lon_pos, lat_pos = np.meshgrid(range(width),range(height))
342
+ print('start transform ..........')
343
+ #lon, lat = rasterio.transform.xy(transform,lat_pos.flatten(),lon_pos.flatten())
344
+ lon, lat = fast_transform_xy(transform,lat_pos.flatten(),lon_pos.flatten())
345
+ print('start dataframe ..........')
346
+ gdf = gpd.GeoDataFrame(ims_transformed.flatten(),
347
+ geometry = gpd.points_from_xy(lon, lat, crs="EPSG:4326"))
348
+ gdf = gdf.rename(columns={0:'im'})
349
+ # return only the non-zero intensity measures
350
+ return gdf[gdf['im'] > 0]
351
+ #return gdf.sort_values(by='im',ascending=False).head(10000)
352
+
353
  @solara.component
354
  def MetricWidget(name, description, value, max_value, render_count):
355
  value, set_value = solara.use_state_or_update(value)
 
390
  extension = fileinfo['name'].split('.')[-1]
391
  if extension == 'xlsx':
392
  df = pd.read_excel(data)
393
+ elif extension in ['tiff','tif']:
394
+ df = read_tiff(data)
395
  else:
396
  json_string = data.decode('utf-8')
397
  json_data = json.loads(json_string)
 
541
  zoom, set_zoom = solara.use_state(default_zoom)
542
  #center, set_center = solara.use_state(default_center)
543
 
544
+ base_map = ipyleaflet.basemaps["Esri"]["WorldImagery"]
545
  base_layer = ipyleaflet.TileLayer.element(url=base_map.build_url())
546
  map_layers = [base_layer]
547
 
tomorrowcities/pages/utilities.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import solara
2
+ import time
3
+ import random
4
+ import json
5
+ import pandas as pd
6
+ import os
7
+ os.environ['USE_PYGEOS'] = '0'
8
+ import geopandas as gpd
9
+ from typing import Tuple, Optional
10
+ import ipyleaflet
11
+ from ipyleaflet import AwesomeIcon, Marker
12
+ import numpy as np
13
+ import logging, sys
14
+ #logging.basicConfig(stream=sys.stderr, level=logging.INFO)
15
+
16
+ from ..backend.engine import compute, compute_power_infra, calculate_metrics
17
+
18
+ app_data = solara.reactive({'df': solara.reactive(None),
19
+ 'gdf': solara.reactive(None),
20
+ 'selected_columns': solara.reactive([]),
21
+ 'lat': solara.reactive(None),
22
+ 'lon': solara.reactive(None),
23
+ 'im': solara.reactive(None)})
24
+
25
+ def import_data(fileinfo: solara.components.file_drop.FileInfo):
26
+ data = fileinfo['data']
27
+ extension = fileinfo['name'].split('.')[-1]
28
+ if extension == 'xlsx':
29
+ df = pd.read_excel(data)
30
+ elif extension == 'csv':
31
+ df = pd.read_csv(data)
32
+ else:
33
+ json_string = data.decode('utf-8')
34
+ json_data = json.loads(json_string)
35
+ if "features" in json_data.keys():
36
+ df = gpd.GeoDataFrame.from_features(json_data['features'])
37
+ else:
38
+ df = pd.read_json(json_string)
39
+
40
+ df.columns = df.columns.str.lower()
41
+
42
+ return df
43
+
44
+
45
+ @solara.component
46
+ def FileDropZone():
47
+ total_progress, set_total_progress = solara.use_state(-1)
48
+ fileinfo, set_fileinfo = solara.use_state(None)
49
+ result, set_result = solara.use_state(solara.Result(True))
50
+
51
+ def load():
52
+ print('loading')
53
+ if fileinfo is not None:
54
+ print('processing file')
55
+ df = import_data(fileinfo)
56
+ print('importing done')
57
+ if df is not None:
58
+ app_data.value['df'].set(df)
59
+ else:
60
+ return False
61
+ return True
62
+
63
+ def progress(x):
64
+ set_total_progress(x)
65
+
66
+ def on_file_deneme(f):
67
+ set_fileinfo(f)
68
+
69
+ result = solara.use_thread(load, dependencies=[fileinfo])
70
+
71
+ solara.Markdown("Step1: Drag and drop your Excel file to the below area.")
72
+ solara.FileDrop(on_total_progress=progress,
73
+ on_file=on_file_deneme,
74
+ lazy=False)
75
+ if total_progress > -1 and total_progress < 100:
76
+ solara.Text(f"Uploading {total_progress}%")
77
+ solara.ProgressLinear(value=total_progress)
78
+ else:
79
+ if result.state == solara.ResultState.FINISHED:
80
+ if result.value:
81
+ solara.Text("Spacer", style={'visibility':'hidden'})
82
+ else:
83
+ solara.Text("Unrecognized file")
84
+ solara.ProgressLinear(value=False)
85
+ elif result.state == solara.ResultState.INITIAL:
86
+ solara.Text("Spacer", style={'visibility':'hidden'})
87
+ solara.ProgressLinear(value=False)
88
+ elif result.state == solara.ResultState.ERROR:
89
+ solara.Text(f'{result.error}')
90
+ solara.ProgressLinear(value=False)
91
+ else:
92
+ solara.Text("Reading the contents")
93
+ solara.ProgressLinear(value=True)
94
+
95
+ @solara.component
96
+ def DataframeDisplayer():
97
+ if app_data.value['df'].value is not None:
98
+ solara.DataFrame(app_data.value['df'].value)
99
+
100
+ @solara.component
101
+ def FieldSelector():
102
+ solara.Markdown("Step 2: Select the field names")
103
+ if app_data.value['df'].value is not None:
104
+ solara.Text("lat")
105
+ solara.ToggleButtonsSingle(value=app_data.value['lat'].value, values=list(app_data.value['df'].value.columns), on_value=app_data.value['lat'].set)
106
+ solara.Text("lon")
107
+ solara.ToggleButtonsSingle(value=app_data.value['lon'].value, values=list(app_data.value['df'].value.columns),on_value=app_data.value['lon'].set)
108
+ solara.Text("im")
109
+ solara.ToggleButtonsSingle(value=app_data.value['im'].value, values=list(app_data.value['df'].value.columns),on_value=app_data.value['im'].set)
110
+
111
+ @solara.component
112
+ def Downloader():
113
+ error_msg, set_error_msg = solara.use_state(None)
114
+ success_msg, set_success_msg = solara.use_state(None)
115
+ def generate():
116
+ set_error_msg(None)
117
+ set_success_msg(None)
118
+ if app_data.value['df'].value is not None:
119
+ if app_data.value['lat'].value is not None:
120
+ if app_data.value['lon'].value is not None:
121
+ if app_data.value['im'].value is not None:
122
+ try:
123
+ lat = app_data.value['lat'].value
124
+ lon = app_data.value['lon'].value
125
+ im = app_data.value['im'].value
126
+ df_conv = app_data.value['df'].value[[lat,lon,im]]
127
+ df_conv = df_conv.rename(columns={lat:'lat',lon:'lon',im:'im'})
128
+ gdf = gpd.GeoDataFrame(df_conv[['im']],
129
+ geometry = gpd.points_from_xy(df_conv[lon], df_conv[lat], crs="EPSG:4326"))
130
+ app_data.value['gdf'].set(gdf)
131
+ set_success_msg("Your file is ready")
132
+ set_error_msg(None)
133
+ except Exception as e:
134
+ app_data.value['gdf'].set(None)
135
+ set_error_msg(repr(e))
136
+ set_success_msg(None)
137
+
138
+ solara.Markdown("Step 3: Clict to generate GeoJSON")
139
+ solara.Button("Generate GeoJSON", icon_name="mdi-cloud-download-outline", color="primary", on_click=generate)
140
+ if error_msg is not None:
141
+ solara.Error(error_msg)
142
+ if success_msg is not None:
143
+ solara.Success(success_msg)
144
+ if app_data.value['gdf'].value is not None:
145
+ file_object = app_data.value['gdf'].value .to_json()
146
+ with solara.FileDownload(file_object, "intensity_blabla.geojson", mime_type="application/geo+json"):
147
+ solara.Button("Click to Downlaod genereated GeoJSON", icon_name="mdi-cloud-download-outline", color="primary")
148
+ @solara.component
149
+ def Utilities():
150
+ with solara.Columns([30,80]):
151
+ FileDropZone()
152
+ FieldSelector()
153
+ Downloader()
154
+ DataframeDisplayer()
155
+
156
+
157
+ @solara.component
158
+ def Page(name: Optional[str] = None, page: int = 0, page_size=100):
159
+ css = """
160
+ .v-input {
161
+ height: 10px;
162
+ }
163
+
164
+ .v-btn-toggle:not(.v-btn-toggle--dense) .v-btn.v-btn.v-size--default {
165
+ height: 24px;
166
+ min-height: 0;
167
+ min-width: 24px;
168
+ }
169
+
170
+ """
171
+ solara.Style(value=css)
172
+ solara.Title("TCDSE » Engine")
173
+
174
+ Utilities()