hkayabilisim commited on
Commit
8f70819
·
1 Parent(s): 40d9e16

feature: added multiple file drop support

Browse files
tomorrowcities/components/file_drop.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import threading
2
+ import typing
3
+ from typing import Callable, List, Optional, Union, cast
4
+
5
+ import traitlets
6
+ from ipyvue import Template
7
+ from ipyvuetify.extra import FileInput
8
+ from ipywidgets import widget_serialization
9
+ from typing_extensions import TypedDict
10
+
11
+ import solara
12
+ import solara.hooks as hooks
13
+
14
+
15
+ class FileInfo(TypedDict):
16
+ name: str
17
+ size: int
18
+ file_obj: typing.BinaryIO
19
+ data: Optional[bytes]
20
+
21
+
22
+ class FileDropZone(FileInput):
23
+ # override to narrow traitlet of FileInput
24
+ template = traitlets.Instance(Template).tag(sync=True, **widget_serialization)
25
+ template_file = (__file__, "file_drop.vue")
26
+ items = traitlets.List(default_value=[]).tag(sync=True)
27
+ label = traitlets.Unicode().tag(sync=True)
28
+ multiple = traitlets.Bool(True).tag(sync=True)
29
+
30
+
31
+ @solara.component
32
+ def _FileDrop(
33
+ label="Drop file(s) here",
34
+ on_total_progress: Optional[Callable[[float], None]] = None,
35
+ on_file: Optional[Callable[[Union[FileInfo, List[FileInfo]]], None]] = None,
36
+ lazy: bool = True,
37
+ multiple: bool = False,
38
+ ):
39
+ """Generic implementation used by FileDrop and FileDropMultiple.
40
+
41
+ If multiple=True, multiple files can be uploaded.
42
+ """
43
+
44
+ file_info, set_file_info = solara.use_state(None)
45
+ wired_files, set_wired_files = solara.use_state(cast(Optional[typing.List[FileInfo]], None))
46
+
47
+ file_drop = FileDropZone.element(label=label, on_total_progress=on_total_progress, on_file_info=set_file_info, multiple=multiple) # type: ignore
48
+
49
+ def wire_files():
50
+ if not file_info:
51
+ return
52
+
53
+ real = cast(FileDropZone, solara.get_widget(file_drop))
54
+
55
+ # workaround for @observe being cleared
56
+ real.version += 1
57
+ real.reset_stats()
58
+
59
+ set_wired_files(cast(typing.List[FileInfo], real.get_files()))
60
+
61
+ solara.use_side_effect(wire_files, [file_info])
62
+
63
+ def handle_file(cancel: threading.Event):
64
+ if not wired_files:
65
+ return
66
+ if on_file:
67
+ for i in range(len(wired_files)):
68
+ if not lazy:
69
+ wired_files[i]["data"] = wired_files[i]["file_obj"].read()
70
+ else:
71
+ wired_files[i]["data"] = None
72
+ if multiple:
73
+ on_file(wired_files)
74
+ else:
75
+ on_file(wired_files[0])
76
+
77
+ result: solara.Result = hooks.use_thread(handle_file, [wired_files])
78
+ if result.error:
79
+ raise result.error
80
+
81
+ return file_drop
82
+
83
+
84
+ @solara.component
85
+ def FileDrop(
86
+ label="Drop file here",
87
+ on_total_progress: Optional[Callable[[float], None]] = None,
88
+ on_file: Optional[Callable[[FileInfo], None]] = None,
89
+ lazy: bool = True,
90
+ ):
91
+ """Region a user can drop a file into for file uploading.
92
+
93
+ If lazy=True, no file content will be loaded into memory,
94
+ nor will any data be transferred by default.
95
+ If lazy=False, file content will be loaded into memory and passed to the `on_file` callback via the `FileInfo.data` attribute.
96
+
97
+
98
+ A file object is of the following argument type:
99
+ ```python
100
+ class FileInfo(typing.TypedDict):
101
+ name: str # file name
102
+ size: int # file size in bytes
103
+ file_obj: typing.BinaryIO
104
+ data: Optional[bytes]: bytes # only present if lazy=False
105
+ ```
106
+
107
+
108
+ ## Arguments
109
+ * `on_total_progress`: Will be called with the progress in % of the file upload.
110
+ * `on_file`: Will be called with a `FileInfo` object, which contains the file `.name`, `.length` and a `.file_obj` object.
111
+ * `lazy`: Whether to load the file contents into memory or not. If `False`,
112
+ the file contents will be loaded into memory via the `.data` attribute of file object(s).
113
+
114
+ """
115
+
116
+ return _FileDrop(label=label, on_total_progress=on_total_progress, on_file=on_file, lazy=lazy, multiple=False)
117
+
118
+
119
+ @solara.component
120
+ def FileDropMultiple(
121
+ label="Drop files here",
122
+ on_total_progress: Optional[Callable[[float], None]] = None,
123
+ on_file: Optional[Callable[[List[FileInfo]], None]] = None,
124
+ lazy: bool = True,
125
+ ):
126
+ """Region a user can drop multiple files into for file uploading.
127
+
128
+ Almost identical to `FileDrop` except that multiple files can be dropped and `on_file` is called
129
+ with a list of `FileInfo` objects.
130
+
131
+ ## Arguments
132
+ * `on_total_progress`: Will be called with the progress in % of the file(s) upload.
133
+ * `on_file`: Will be called with a `List[FileInfo]`.
134
+ Each `FileInfo` contains the file `.name`, `.length`, `.file_obj` object, and `.data` attributes.
135
+ * `lazy`: Whether to load the file contents into memory or not.
136
+
137
+ """
138
+
139
+ return _FileDrop(label=label, on_total_progress=on_total_progress, on_file=on_file, lazy=lazy, multiple=True)
tomorrowcities/components/file_drop.vue ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <template>
2
+ <div ref="dropzone" class="solara-file-drop" effectAllowed="move">
3
+ <template v-if="file_info && file_info.length > 0">
4
+ <template v-if="multiple">
5
+ <div v-for="file in file_info">
6
+ {{ file.name }}
7
+ </div>
8
+ </template>
9
+ <template v-else>
10
+ {{ file_info[0].name }}
11
+ </template>
12
+ </template>
13
+ <template v-else>
14
+ {{ label }}
15
+ </template>
16
+ </div>
17
+ </template>
18
+
19
+ <script>
20
+ module.exports = {
21
+ mounted() {
22
+ this.chunk_size = 2 * 1024 * 1024;
23
+ this.$refs.dropzone.addEventListener('dragover', event => {
24
+ event.preventDefault();
25
+ });
26
+
27
+ this.$refs.dropzone.addEventListener('drop', async event => {
28
+ event.preventDefault();
29
+ const items = await Promise.all([...event.dataTransfer.items]);
30
+ const files = items.map(i => i.webkitGetAsEntry())
31
+ const fileHolders = files.filter(f => f.isFile)
32
+ const nativeFilesPromises = fileHolders.map(fileHolder => new Promise((rs, rj) => fileHolder.file(rs, rj)))
33
+ const nativeFiles = await Promise.all(nativeFilesPromises)
34
+
35
+ this.native_file_info = nativeFiles
36
+ this.file_info = this.native_file_info.map(
37
+ ({name, size}) => ({
38
+ name,
39
+ isFile: true,
40
+ size,
41
+ }));
42
+ });
43
+ },
44
+ methods: {
45
+ jupyter_clear() {
46
+ this.native_file_info = [];
47
+ this.file_info = [];
48
+ },
49
+ jupyter_read(chunk) {
50
+ const {id, file_index, offset, length} = chunk;
51
+ let to_do = length;
52
+ let sub_offset = offset;
53
+
54
+ (async () => {
55
+ while (to_do > 0) {
56
+ console.log(this.chunk_size, to_do);
57
+ const sub_length = Math.min(to_do, this.chunk_size);
58
+
59
+ const file = this.native_file_info[file_index];
60
+ const blob = file.slice(sub_offset, sub_offset + sub_length);
61
+ const buff = await blob.arrayBuffer();
62
+
63
+ const msg = {id, file_index, offset: sub_offset, length: sub_length}
64
+ this.upload(msg, [buff]);
65
+
66
+ to_do -= sub_length
67
+ sub_offset += sub_length
68
+ }
69
+ })();
70
+ }
71
+ }
72
+ }
73
+ </script>
74
+
75
+ <style id="solara-file-drop">
76
+ .solara-file-drop {
77
+ height: 100px;
78
+ border: 1px dashed gray;
79
+ margin: 8px 0;
80
+ padding: 8px;
81
+ overflow: auto;
82
+ }
83
+ </style>
tomorrowcities/pages/engine.py CHANGED
@@ -26,6 +26,7 @@ from .settings import threshold_flood, threshold_flood_distance, threshold_road_
26
  from ..backend.engine import compute, compute_power_infra, compute_road_infra, calculate_metrics, generate_exposure
27
  from ..backend.utils import building_preprocess, identity_preprocess, ParameterFile
28
  from .utilities import S3FileBrowser, extension_list, extension_list_w_dots
 
29
  from .docs import data_import_help
30
  import ipywidgets
31
  import ipydatagrid
@@ -1374,7 +1375,7 @@ def ImportDataZone():
1374
  the below area. Supported formats are Excel, GeoTIFF, JSON, GeoJSON, and GEM XML.
1375
  For more information, please refer to [Data Formats](/docs/data).
1376
  ''')
1377
- solara.FileDrop(on_total_progress=progress,
1378
  on_file=on_file_deneme,
1379
  lazy=False)
1380
 
 
26
  from ..backend.engine import compute, compute_power_infra, compute_road_infra, calculate_metrics, generate_exposure
27
  from ..backend.utils import building_preprocess, identity_preprocess, ParameterFile
28
  from .utilities import S3FileBrowser, extension_list, extension_list_w_dots
29
+ from ..components.file_drop import FileDropMultiple
30
  from .docs import data_import_help
31
  import ipywidgets
32
  import ipydatagrid
 
1375
  the below area. Supported formats are Excel, GeoTIFF, JSON, GeoJSON, and GEM XML.
1376
  For more information, please refer to [Data Formats](/docs/data).
1377
  ''')
1378
+ FileDropMultiple(on_total_progress=progress,
1379
  on_file=on_file_deneme,
1380
  lazy=False)
1381