Hosted pages
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

interactive map

+289 -2
+289 -2
index.html
··· 9 9 <title>OCaml notebook</title></head> 10 10 <body> 11 11 <h1>OxCaml Notebook</h1> 12 - <x-ocaml> 13 - let _ = Printf.printf "Hello, world\n%!";; 12 + 13 + <x-ocaml mode="interactive">#require &quot;tessera-zarr-jsoo&quot;;; 14 + #require &quot;tessera-viz-jsoo&quot;;; 15 + #require &quot;tessera-tfjs&quot;;; 16 + #require &quot;js_top_worker-widget-leaflet&quot;;; 17 + open Widget_leaflet;; 18 + register ();; 19 + (* Load fzstd (Zstd decompressor) and TensorFlow.js *) 20 + let () = 21 + let open Js_of_ocaml in 22 + let import url : unit = Js.Unsafe.fun_call 23 + (Js.Unsafe.get Js.Unsafe.global (Js.string &quot;importScripts&quot;)) 24 + [| Js.Unsafe.inject (Js.string url) |] in 25 + import &quot;https://cdn.jsdelivr.net/npm/fzstd@0.1.1/umd/index.js&quot;; 26 + import &quot;https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4/dist/tf.min.js&quot;</x-ocaml><h2 id="reactive-pipeline"><a href="#reactive-pipeline" class="anchor"></a>Reactive pipeline</h2><p>The entire notebook is a single reactive pipeline. Drawing a bounding box on the map triggers: fetch embeddings → PCA → overlay. Clicking on the map adds training points. The classify button runs kNN and updates the overlay.</p><x-ocaml mode="interactive">let class_colors = [| &quot;#2196F3&quot;; &quot;#4CAF50&quot;; &quot;#FF9800&quot;; &quot;#9C27B0&quot;; &quot;#F44336&quot;; 27 + &quot;#00BCD4&quot;; &quot;#795548&quot;; &quot;#607D8B&quot; |] 28 + 29 + (* --- Status display --- *) 30 + 31 + let status_view text = 32 + let open Widget.View in 33 + Element { tag = &quot;div&quot;; attrs = [Style (&quot;padding&quot;, &quot;8px&quot;); Style (&quot;font-family&quot;, &quot;monospace&quot;)]; 34 + children = [Text text] } 35 + 36 + let () = Widget.display ~id:&quot;status&quot; ~handlers:[] (status_view &quot;Draw a rectangle on the map.&quot;) 37 + 38 + (* --- FRP signals --- *) 39 + 40 + let bbox_signal, set_bbox = 41 + Note.S.create ~eq:(fun a b -&gt; a = b) (None : Geotessera.bbox option) 42 + 43 + let year_signal, set_year = Note.S.create ~eq:Int.equal 2024 44 + 45 + let current_class_signal, set_current_class = Note.S.create ~eq:Int.equal 0 46 + let class_names_signal, set_class_names = 47 + Note.S.create ~eq:(fun a b -&gt; a = b) [| &quot;water&quot;; &quot;land&quot; |] 48 + let training_points_signal, set_training_points = 49 + Note.S.create ~eq:(fun _ _ -&gt; false) ([] : (float * float * int) list) 50 + 51 + (* --- Map widget (typed interface) --- *) 52 + 53 + let () = Widget.display ~id:&quot;debug&quot; ~handlers:[] (status_view &quot;No bbox drawn yet.&quot;) 54 + 55 + let map_ref : Leaflet_map.t option ref = ref None 56 + let map_get () = match !map_ref with Some m -&gt; m | None -&gt; failwith &quot;map not ready&quot; 57 + 58 + let map = Leaflet_map.create 59 + ~center:(52.2, 0.12) ~zoom:13 ~height:&quot;500px&quot; 60 + ~on_move:(fun info -&gt; 61 + Widget.update ~id:&quot;status&quot; 62 + (status_view (Printf.sprintf &quot;Center: %.5f, %.5f Zoom: %d Bounds: S%.5f W%.5f N%.5f E%.5f&quot; 63 + info.center.lat info.center.lng info.zoom 64 + info.bounds.south info.bounds.west info.bounds.north info.bounds.east))) 65 + ~on_bbox_drawn:(fun b -&gt; 66 + Widget.update ~id:&quot;debug&quot; 67 + (status_view (Printf.sprintf &quot;Drawn bbox: S%.5f W%.5f N%.5f E%.5f&quot; 68 + b.south b.west b.north b.east)); 69 + set_bbox (Some Geotessera.{ 70 + min_lat = b.south; min_lon = b.west; 71 + max_lat = b.north; max_lon = b.east })) 72 + ~on_click:(fun pt -&gt; 73 + let cls = Note.S.value current_class_signal in 74 + let points = Note.S.value training_points_signal in 75 + set_training_points ((pt.lat, pt.lng, cls) :: points); 76 + let color = class_colors.(cls mod Array.length class_colors) in 77 + let names = Note.S.value class_names_signal in 78 + Leaflet_map.add_marker (map_get ()) pt ~color ~label:names.(cls) ()) 79 + () 80 + let () = map_ref := Some map 81 + 82 + let () = Leaflet_map.enable_bbox_draw map 83 + 84 + (* --- Async pipeline: bbox → fetch → mosaic --- *) 85 + 86 + type mosaic_data = { 87 + mat : Linalg.mat; 88 + h : int; 89 + w : int; 90 + bounds : Leaflet_map.bounds; 91 + } 92 + 93 + let downsample mat ~h ~w ~max_pixels = 94 + let n = h * w in 95 + if n &lt;= max_pixels then (mat, h, w) 96 + else 97 + let stride = int_of_float (ceil (sqrt (float_of_int n /. float_of_int max_pixels))) in 98 + let h' = (h + stride - 1) / stride in 99 + let w' = (w + stride - 1) / stride in 100 + let out = Linalg.create_mat ~rows:(h' * w') ~cols:mat.Linalg.cols in 101 + for i = 0 to h' - 1 do 102 + for j = 0 to w' - 1 do 103 + let si = min (i * stride) (h - 1) in 104 + let sj = min (j * stride) (w - 1) in 105 + for f = 0 to mat.Linalg.cols - 1 do 106 + Linalg.mat_set out (i * w' + j) f 107 + (Linalg.mat_get mat (si * w + sj) f) 108 + done 109 + done 110 + done; 111 + (out, h', w') 112 + 113 + let bbox_year_signal = 114 + Note.S.l2 (fun b y -&gt; Option.map (fun b -&gt; (b, y)) b) bbox_signal year_signal 115 + 116 + let mosaic_signal : mosaic_data Tessera_zarr_jsoo.Frp_async.state Note.signal = 117 + Tessera_zarr_jsoo.Frp_async.async_bind bbox_year_signal 118 + (fun (bbox, year) progress -&gt; 119 + progress (Printf.sprintf &quot;Opening Zarr v2 store (year %d)...&quot; year); 120 + let open Lwt.Syntax in 121 + let* store = Tessera_zarr_jsoo.open_store ~year () in 122 + let* (mat_full, h_full, w_full, geo_bounds) = 123 + Tessera_zarr.fetch_region ~progress ~year ~store bbox in 124 + progress (Printf.sprintf &quot;Fetched %d×%d. Downsampling...&quot; h_full w_full); 125 + let (mat, h, w) = downsample mat_full ~h:h_full ~w:w_full ~max_pixels:1_000_000 in 126 + let bounds = Leaflet_map.{ 127 + south = geo_bounds.Geotessera.min_lat; 128 + north = geo_bounds.Geotessera.max_lat; 129 + west = geo_bounds.Geotessera.min_lon; 130 + east = geo_bounds.Geotessera.max_lon; 131 + } in 132 + Lwt.return { mat; h; w; bounds }) 133 + 134 + (* PCA: runs when mosaic becomes Ready *) 135 + let pca_signal : Linalg.mat option Note.signal = 136 + Note.S.map (function 137 + | Tessera_zarr_jsoo.Frp_async.Ready m -&gt; 138 + Some (Tfjs.pca m.mat ~n_components:3) 139 + | _ -&gt; None 140 + ) mosaic_signal 141 + 142 + (* Overlay: update map when PCA result changes *) 143 + let _overlay_logr = 144 + let logr = Note.Logr.create ( 145 + Note.Logr.app (Note.Logr.const (fun pca_opt -&gt; 146 + match pca_opt, Note.S.value mosaic_signal with 147 + | Some proj, Tessera_zarr_jsoo.Frp_async.Ready m -&gt; 148 + let img = Viz.pca_to_rgba ~width:m.w ~height:m.h proj in 149 + let url = Viz_jsoo.to_data_url img in 150 + Leaflet_map.add_image_overlay map ~url ~bounds:m.bounds ~opacity:0.7 () 151 + | _ -&gt; () 152 + )) (Note.S.obs pca_signal)) 153 + in Note.Logr.hold logr 154 + 155 + (* Status: update display reactively *) 156 + let _status_logr = 157 + let logr = Note.Logr.create ( 158 + Note.Logr.app (Note.Logr.const (fun state -&gt; 159 + let text = match state with 160 + | Tessera_zarr_jsoo.Frp_async.Idle -&gt; 161 + &quot;Draw a rectangle on the map.&quot; 162 + | Loading msg -&gt; Printf.sprintf &quot;Loading: %s&quot; msg 163 + | Ready m -&gt; 164 + Printf.sprintf &quot;Ready: %d×%d mosaic. Click to add training points.&quot; m.h m.w 165 + | Error msg -&gt; Printf.sprintf &quot;Error: %s&quot; msg 166 + in 167 + Widget.update ~id:&quot;status&quot; (status_view text) 168 + )) (Note.S.obs mosaic_signal)) 169 + in Note.Logr.hold logr</x-ocaml><h2 id="year-selection"><a href="#year-selection" class="anchor"></a>Year selection</h2><p>Select the year for the GeoTessera embeddings (2017–2025). Changing the year re-fetches the current bounding box.</p><x-ocaml mode="interactive">let make_year_buttons () = 170 + let open Widget.View in 171 + let current = Note.S.value year_signal in 172 + let buttons = List.map (fun y -&gt; 173 + let border = if y = current then &quot;3px solid black&quot; else &quot;1px solid #ccc&quot; in 174 + Element { tag = &quot;button&quot;; 175 + attrs = [ 176 + Handler (&quot;click&quot;, &quot;year_&quot; ^ string_of_int y); 177 + Style (&quot;margin&quot;, &quot;2px&quot;); Style (&quot;padding&quot;, &quot;6px 12px&quot;); 178 + Style (&quot;border&quot;, border); Style (&quot;border-radius&quot;, &quot;4px&quot;); 179 + Style (&quot;cursor&quot;, &quot;pointer&quot;); 180 + ]; 181 + children = [Text (string_of_int y)] } 182 + ) [2017; 2018; 2019; 2020; 2021; 2022; 2023; 2024; 2025] in 183 + Element { tag = &quot;div&quot;; 184 + attrs = [Style (&quot;padding&quot;, &quot;8px&quot;)]; 185 + children = Element { tag = &quot;b&quot;; attrs = []; children = [Text &quot;Year: &quot;] } :: buttons } 186 + 187 + let () = 188 + let handlers = List.map (fun y -&gt; 189 + &quot;year_&quot; ^ string_of_int y, (fun (_v : string option) -&gt; 190 + set_year y; 191 + Widget.update ~id:&quot;year-buttons&quot; (make_year_buttons ())) 192 + ) [2017; 2018; 2019; 2020; 2021; 2022; 2023; 2024; 2025] in 193 + Widget.display ~id:&quot;year-buttons&quot; ~handlers (make_year_buttons ())</x-ocaml><h2 id="label-training-points"><a href="#label-training-points" class="anchor"></a>Label training points</h2><p>Click on the map to add training points. Use the buttons below to switch classes, classify, or clear.</p><x-ocaml mode="interactive">let make_class_buttons () = 194 + let open Widget.View in 195 + let names = Note.S.value class_names_signal in 196 + let cls = Note.S.value current_class_signal in 197 + let buttons = Array.to_list (Array.mapi (fun i name -&gt; 198 + let color = class_colors.(i mod Array.length class_colors) in 199 + let border = if i = cls then &quot;3px solid black&quot; else &quot;1px solid #ccc&quot; in 200 + Element { tag = &quot;button&quot;; 201 + attrs = [ 202 + Handler (&quot;click&quot;, &quot;class_&quot; ^ string_of_int i); 203 + Style (&quot;margin&quot;, &quot;4px&quot;); Style (&quot;padding&quot;, &quot;8px 16px&quot;); 204 + Style (&quot;background&quot;, color); Style (&quot;color&quot;, &quot;white&quot;); 205 + Style (&quot;border&quot;, border); Style (&quot;border-radius&quot;, &quot;4px&quot;); 206 + Style (&quot;cursor&quot;, &quot;pointer&quot;); 207 + ]; 208 + children = [Text name] } 209 + ) names) in 210 + Element { tag = &quot;div&quot;; 211 + attrs = [Style (&quot;padding&quot;, &quot;8px&quot;)]; 212 + children = 213 + Element { tag = &quot;b&quot;; attrs = []; children = [Text &quot;Active class: &quot;] } 214 + :: buttons 215 + @ [Element { tag = &quot;button&quot;; 216 + attrs = [ Handler (&quot;click&quot;, &quot;add_class&quot;); 217 + Style (&quot;margin&quot;, &quot;4px&quot;); Style (&quot;padding&quot;, &quot;8px 16px&quot;); 218 + Style (&quot;border&quot;, &quot;1px dashed #999&quot;); Style (&quot;border-radius&quot;, &quot;4px&quot;); 219 + Style (&quot;cursor&quot;, &quot;pointer&quot;) ]; 220 + children = [Text &quot;+ Add class&quot;] }; 221 + Element { tag = &quot;button&quot;; 222 + attrs = [ Handler (&quot;click&quot;, &quot;classify&quot;); 223 + Style (&quot;margin&quot;, &quot;4px&quot;); Style (&quot;padding&quot;, &quot;8px 16px&quot;); 224 + Style (&quot;background&quot;, &quot;#FF5722&quot;); Style (&quot;color&quot;, &quot;white&quot;); 225 + Style (&quot;border&quot;, &quot;none&quot;); Style (&quot;border-radius&quot;, &quot;4px&quot;); 226 + Style (&quot;cursor&quot;, &quot;pointer&quot;); Style (&quot;font-weight&quot;, &quot;bold&quot;) ]; 227 + children = [Text &quot;Classify&quot;] }; 228 + Element { tag = &quot;button&quot;; 229 + attrs = [ Handler (&quot;click&quot;, &quot;clear_points&quot;); 230 + Style (&quot;margin&quot;, &quot;4px&quot;); Style (&quot;padding&quot;, &quot;8px 16px&quot;); 231 + Style (&quot;border&quot;, &quot;1px solid #ccc&quot;); Style (&quot;border-radius&quot;, &quot;4px&quot;); 232 + Style (&quot;cursor&quot;, &quot;pointer&quot;) ]; 233 + children = [Text &quot;Clear all&quot;] }] 234 + } 235 + 236 + let () = 237 + let names = Note.S.value class_names_signal in 238 + let handlers = 239 + Array.to_list (Array.mapi (fun i _name -&gt; 240 + &quot;class_&quot; ^ string_of_int i, (fun (_v : string option) -&gt; 241 + set_current_class i; 242 + Widget.update ~id:&quot;class-buttons&quot; (make_class_buttons ())) 243 + ) names) 244 + @ [ 245 + &quot;add_class&quot;, (fun _v -&gt; 246 + let names = Note.S.value class_names_signal in 247 + let n = Array.length names in 248 + set_class_names (Array.append names [| &quot;class_&quot; ^ string_of_int n |]); 249 + set_current_class n; 250 + Widget.update ~id:&quot;class-buttons&quot; (make_class_buttons ())); 251 + &quot;classify&quot;, (fun _v -&gt; 252 + let points = Note.S.value training_points_signal in 253 + (match Note.S.value mosaic_signal with 254 + | Tessera_zarr_jsoo.Frp_async.Ready m when points &lt;&gt; [] -&gt; 255 + Widget.update ~id:&quot;status&quot; 256 + (status_view (Printf.sprintf &quot;Classifying with %d points...&quot; (List.length points))); 257 + let n_train = List.length points in 258 + let train_mat = Linalg.create_mat ~rows:n_train ~cols:m.mat.Linalg.cols in 259 + let train_labels = Array.make n_train 0 in 260 + List.iteri (fun i (lat, lng, cls) -&gt; 261 + let row = int_of_float ((m.bounds.north -. lat) /. 262 + (m.bounds.north -. m.bounds.south) *. float_of_int m.h) in 263 + let col = int_of_float ((lng -. m.bounds.west) /. 264 + (m.bounds.east -. m.bounds.west) *. float_of_int m.w) in 265 + let row = max 0 (min (m.h - 1) row) in 266 + let col = max 0 (min (m.w - 1) col) in 267 + let src = (row * m.w + col) * m.mat.Linalg.cols in 268 + let dst = i * m.mat.Linalg.cols in 269 + for j = 0 to m.mat.Linalg.cols - 1 do 270 + Bigarray.Array1.set train_mat.Linalg.data (dst + j) 271 + (Bigarray.Array1.get m.mat.Linalg.data (src + j)) 272 + done; 273 + train_labels.(i) &lt;- cls 274 + ) points; 275 + let model = Linalg.knn_fit ~embeddings:train_mat ~labels:train_labels in 276 + let k = min 5 n_train in 277 + let result = Linalg.knn_predict model ~k m.mat in 278 + let n_classes = Array.fold_left max 0 train_labels + 1 in 279 + let colors = List.init n_classes (fun i -&gt; 280 + (i, Viz.color_of_hex class_colors.(i mod Array.length class_colors))) in 281 + let img = Viz.classification_to_rgba 282 + ~predictions:result.Linalg.predictions 283 + ~colors ~width:m.w ~height:m.h () in 284 + let url = Viz_jsoo.to_data_url img in 285 + Leaflet_map.add_image_overlay map ~url ~bounds:m.bounds ~opacity:0.7 (); 286 + Widget.update ~id:&quot;status&quot; 287 + (status_view (Printf.sprintf &quot;Classification complete! %d classes, %d points.&quot; 288 + n_classes n_train)) 289 + | _ -&gt; 290 + Widget.update ~id:&quot;status&quot; (status_view &quot;Need mosaic + training points first.&quot;))); 291 + &quot;clear_points&quot;, (fun _v -&gt; 292 + set_training_points []; 293 + Leaflet_map.clear_markers map; 294 + Widget.update ~id:&quot;status&quot; (status_view &quot;Cleared all training points.&quot;)); 295 + ] 296 + in 297 + Widget.display ~id:&quot;class-buttons&quot; ~handlers (make_class_buttons ())</x-ocaml></div></main> 298 + 299 + 300 + 14 301 15 302 </body> 16 303 </html>