@@ -59,7 +59,7 @@ def _hex(rgb):
5959# params.json. "clustering" picks the centre-spacing multiple in post-processing.
6060def presets_for (name : str , params : dict ) -> list [dict ]:
6161 base = [
62- {"id" : "baseline" , "label" : "Baseline" , "note" : "The scenario's own params.json, as shipped." },
62+ {"id" : "baseline" , "label" : "Baseline run " , "note" : "The scenario's own params.json, as shipped." },
6363 {"id" : "walk800" , "label" : "Longer walk (800 m)" ,
6464 "note" : "Centres serve an 800 m walk; growth reaches further from each centre." ,
6565 "overrides" : {"centre_walk_m" : 800.0 , "green_walk_m" : max (400.0 , params .get ("green_walk_m" , 400.0 ))}},
@@ -215,6 +215,43 @@ def render_png(codes, layers, sub, gran, path):
215215 im .save (path , optimize = True )
216216
217217
218+ _SCHEMA_KEYS = (
219+ "crs" , "grid_size_m" , "max_iterations" , "target_population" , "build_prob" , "dispersal" ,
220+ "random_seed" , "centre_walk_m" , "green_walk_m" , "optimise_centres" , "centre_m2_per_person" ,
221+ "min_settlement_ha" , "min_green_span_m" , "densities_km2" , "shares" , "ensemble" , "ensemble_runs" ,
222+ )
223+
224+
225+ def merged_formal_params (params : dict , preset : dict , entry_name : str ) -> dict :
226+ """The FULL-resolution parameter set for a preset, in the plugin's params schema, so the file
227+ downloads straight into the dialog's Load parameters button. Post-processing choices that are
228+ not dialog parameters (the clustering option) are noted rather than encoded."""
229+ p = {k : params [k ] for k in _SCHEMA_KEYS if k in params }
230+ for k , v in preset .get ("overrides" , {}).items ():
231+ if k in ("shares" , "densities_km2" ):
232+ p [k ] = v
233+ elif k != "clustering" :
234+ p [k ] = v
235+ p ["schema" ] = "isobenefit-params/1"
236+ p ["name" ] = f"{ entry_name } _{ preset ['id' ]} "
237+ note = preset ["note" ]
238+ if preset .get ("overrides" , {}).get ("clustering" ):
239+ note += " In QGIS, both clustering options are always written; open the tightly-clustered output layer."
240+ p ["notes" ] = note
241+ return p
242+
243+
244+ def existing_panel (sub ):
245+ """The place as downloaded, before any simulated growth."""
246+ plan = np .zeros_like (sub ["state" ], np .uint8 )
247+ plan [sub ["state" ] == 0 ] = G .PLAN_GREEN
248+ plan [sub ["origin" ] == 0 ] = G .PLAN_GREEN
249+ plan [sub ["origin" ] == 1 ] = G .PLAN_EXIST_BUILT
250+ for r , c in sub ["seeds" ]:
251+ plan [r , c ] = G .PLAN_EXIST_CENTRE
252+ return plan
253+
254+
218255def entry_for (folder : str , extent_key : str , extent , params , layers , title , subtitle ):
219256 name = os .path .basename (folder ) + ("" if extent_key == "main" else f"_{ extent_key } " )
220257 span = max (extent .bounds [2 ] - extent .bounds [0 ], extent .bounds [3 ] - extent .bounds [1 ])
@@ -223,19 +260,40 @@ def entry_for(folder: str, extent_key: str, extent, params, layers, title, subti
223260 print (f"{ name } : grid { sub ['cols' ]} x{ sub ['rows' ]} at { gran :.0f} m, { len (sub ['seeds' ])} centre seeds" )
224261 p = dict (params , _gran = gran )
225262 presets_out = []
263+
264+ # panel 0: the existing situation, so every comparison starts from the before-picture
265+ rel = f"{ name } /existing.png"
266+ render_png (existing_panel (sub ), layers , sub , gran , os .path .join (OUT , rel ))
267+ presets_out .append ({
268+ "id" : "existing" , "label" : "Existing (before growth)" ,
269+ "note" : "The place as downloaded: existing fabric muted, its mixed-use centres magenta. "
270+ "No simulation has run." ,
271+ "image" : rel , "metrics" : None , "settings" : None , "params_file" : None ,
272+ })
273+
226274 for preset in presets_for (name , params ):
227275 disp , metrics = run_preset (sub , p , preset )
228276 rel = f"{ name } /{ preset ['id' ]} .png"
229277 render_png (disp , layers , sub , gran , os .path .join (OUT , rel ))
230278 keep = {k : round (float (metrics .get (k , 0 )), 3 ) for k in
231279 ("served_coverage" , "centre_access" , "green_access" , "population" ,
232280 "centre_m2_per_person" , "green_m2_per_person" , "built_cells" )}
281+ formal = merged_formal_params (params , preset , name )
282+ params_rel = f"{ name } /{ preset ['id' ]} _params.json"
283+ with open (os .path .join (OUT , params_rel ), "w" , encoding = "utf-8" ) as fh :
284+ json .dump (formal , fh , indent = 2 , ensure_ascii = False )
233285 presets_out .append ({"id" : preset ["id" ], "label" : preset ["label" ], "note" : preset ["note" ],
234- "overrides" : preset .get ("overrides" , {}), "image" : rel , "metrics" : keep })
286+ "overrides" : preset .get ("overrides" , {}), "image" : rel , "metrics" : keep ,
287+ "settings" : {k : v for k , v in formal .items () if k not in ("schema" , "notes" )},
288+ "params_file" : params_rel })
235289 print (f" { preset ['id' ]} : served { keep ['served_coverage' ]:.0%} , pop { keep ['population' ]:,.0f} " )
236290 return {"id" : name , "title" : title , "subtitle" : subtitle ,
237291 "grid" : f"{ sub ['cols' ]} x{ sub ['rows' ]} cells at { gran :.0f} m (preview resolution)" ,
238- "seed" : int (params .get ("random_seed" , 42 )), "presets" : presets_out }
292+ "seed" : int (params .get ("random_seed" , 42 )),
293+ "folder" : os .path .basename (folder ),
294+ "github" : f"https://github.com/UCL/BSP-isobenefit-qgis-plugin/tree/main/scenarios/{ os .path .basename (folder )} " ,
295+ "zip" : f"scenarios/{ os .path .basename (folder )} .zip" ,
296+ "presets" : presets_out }
239297
240298
241299TITLES = {
@@ -268,6 +326,21 @@ def main():
268326 with open (bpath , encoding = "utf-8" ) as fh :
269327 pp = json .load (fh )
270328 entries .append (entry_for (folder , key , extent , pp , layers , title , subtitle ))
329+ # one downloadable ZIP per scenario folder (layers + params presets), served by the site
330+ import zipfile
331+
332+ zip_dir = os .path .join (REPO , "website" , "public" , "scenarios" )
333+ os .makedirs (zip_dir , exist_ok = True )
334+ for folder in folders :
335+ base = os .path .basename (folder )
336+ zpath = os .path .join (zip_dir , f"{ base } .zip" )
337+ with zipfile .ZipFile (zpath , "w" , zipfile .ZIP_DEFLATED ) as zf :
338+ for fname in sorted (os .listdir (folder )):
339+ if fname .startswith ("_" ) or fname .endswith (".qgz" ):
340+ continue # caches and personal QGIS projects stay out
341+ zf .write (os .path .join (folder , fname ), arcname = f"{ base } /{ fname } " )
342+ print (f"{ zpath } : { os .path .getsize (zpath ) // 1024 } kB" )
343+
271344 os .makedirs (OUT , exist_ok = True )
272345 with open (os .path .join (OUT , "gallery.json" ), "w" , encoding = "utf-8" ) as fh :
273346 json .dump ({"entries" : entries }, fh , indent = 1 )
0 commit comments