diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 3a22c32c..353b33ab 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,4 +1,4 @@ -name: Docker +name: šŸ“¦ Build & Test Docker Image on: push: @@ -21,7 +21,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: lfs: true diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 6dac74e4..4f9bc61d 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,4 +1,4 @@ -name: macOS +name: 🧪 Test Native (macOS) on: push: @@ -13,7 +13,7 @@ jobs: runs-on: macos-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: lfs: true diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 86ff76d2..fa1e2725 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: Linux +name: 🧪 Test Native (Linux) on: push: @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: lfs: true # we use LFS to store large files (e.g. data.nc) diff --git a/Dockerfile b/Dockerfile index fb1225d7..640f8c12 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,49 +1,103 @@ -FROM ubuntu:22.04 - -# Install build tools, ForeFire dependencies, AND Python environment -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - build-essential \ - libnetcdf-c++4-dev \ - cmake \ - curl \ - python3 \ - python3-pip \ - python3-dev \ - git && \ - rm -rf /var/lib/apt/lists/* - -# Install Python dependencies for testing -RUN pip3 install --no-cache-dir lxml xarray netCDF4 +ARG UBUNTU_IMAGE=ubuntu:24.04 -WORKDIR /forefire -ENV FOREFIREHOME=/forefire +# Stage 1: builder +FROM ${UBUNTU_IMAGE} AS builder + +ARG DEBIAN_FRONTEND=noninteractive + +# Keep the BuildKit apt cache mount populated across builds: disable +# docker-clean's post-install hooks AND tell apt to keep downloaded .debs. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean \ + && echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' \ + > /etc/apt/apt.conf.d/keep-cache \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + libnetcdf-c++4-dev \ + python3-dev \ + python3-pip + +WORKDIR /build + +# LICENSE is required by CPack at configure time (CMakeLists includes CPack). +COPY --link CMakeLists.txt LICENSE ./ +COPY --link app/ ./app/ +# tools/ is needed at configure time: CMakeLists.txt invokes +# tools/check_all_lfs.bash and references tools/runANN/ANNTest.cpp. +COPY --link tools/ ./tools/ +COPY --link src/ ./src/ + +RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ + && cmake --build build -j"$(nproc)" + +COPY --link bindings/ ./bindings/ -# Copy build configuration first (rarely changes) -COPY CMakeLists.txt cmake-build.sh LICENSE ./ +RUN --mount=type=cache,target=/root/.cache/pip \ + pip3 wheel --no-deps --wheel-dir /wheels ./bindings/python -# Copy source code and applications -COPY src/ ./src/ -COPY app/ ./app/ -COPY tools/runANN/ ./tools/runANN/ -# Build and install the ForeFire C++ library -RUN sh cmake-build.sh +# Stage 2: runtime +FROM ${UBUNTU_IMAGE} -# Add the main forefire executable to the PATH -RUN cp /forefire/bin/forefire /usr/local/bin/ +ARG DEBIAN_FRONTEND=noninteractive -# Use pip to install the Python bindings -COPY bindings/ ./bindings/ -RUN pip3 install ./bindings/python +ENV FOREFIREHOME=/forefire \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 -# Copy everything else (changes most frequently - docs, tests, etc.) -COPY . . +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + rm -f /etc/apt/apt.conf.d/docker-clean \ + && echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' \ + > /etc/apt/apt.conf.d/keep-cache \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + libnetcdf-c++4-1 \ + python3 \ + python3-pip \ + ca-certificates \ + curl -# WE COULD USE A CUSTOM ENTRYPOINT SCRIPT TO START THE HTTP SERVER AND RUN THE DEMO SIMULATION -# COPY tools/devops/entrypoint.sh /usr/local/bin/entrypoint.sh -# RUN chmod +x /usr/local/bin/entrypoint.sh -# ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] +RUN --mount=type=cache,target=/root/.cache/pip \ + pip3 install --break-system-packages \ + lxml xarray netCDF4 + +COPY --from=builder --link /build/bin/forefire /usr/local/bin/forefire +COPY --from=builder --link /build/lib/libforefireL.so /usr/local/lib/ +COPY --from=builder --link /wheels/ /tmp/wheels/ + +RUN --mount=type=cache,target=/root/.cache/pip \ + pip3 install --break-system-packages /tmp/wheels/*.whl \ + && rm -rf /tmp/wheels \ + && ldconfig + +# Non-root user at UID 1000 so files written to mounted volumes get the +# host user's ownership. Ubuntu 24.04 ships a default `ubuntu` user at +# 1000, so we drop it first to free the slot. The /forefire/bin symlink +# keeps `../../bin/forefire` working in test scripts (tests/runff/ff-run.bash:16). +RUN userdel --remove ubuntu 2>/dev/null || true \ + && groupadd --gid 1000 forefire \ + && useradd --gid 1000 --uid 1000 --create-home --shell /bin/bash forefire \ + && mkdir -p /forefire/bin \ + && ln -s /usr/local/bin/forefire /forefire/bin/forefire \ + && chown -R 1000:1000 /forefire + +COPY --chown=1000:1000 app/ /forefire/app/ +COPY --chown=1000:1000 bindings/ /forefire/bindings/ +COPY --chown=1000:1000 tests/ /forefire/tests/ +COPY --chown=1000:1000 tools/ /forefire/tools/ + +LABEL org.opencontainers.image.source="https://github.com/forefireAPI/forefire" \ + org.opencontainers.image.documentation="https://forefire.readthedocs.io/" \ + org.opencontainers.image.description="ForeFire is an open-source code for wildland fire spread models" \ + org.opencontainers.image.licenses="GPL-3.0-or-later" \ + org.opencontainers.image.title="forefire" + +USER forefire +WORKDIR /forefire -# Set the entrypoint to bash for interactive sessions -CMD ["bash"] \ No newline at end of file +EXPOSE 8000 +CMD ["bash"] diff --git a/FUELCOVER.md b/FUELCOVER.md new file mode 100644 index 00000000..f01b41c3 --- /dev/null +++ b/FUELCOVER.md @@ -0,0 +1,88 @@ +## ESA CCI Land Cover Classes (ESA CCI Land Cover Product User Guide, Version 2.0, 2017) + +| Map code | Land Cover Class | LCCS code | Definition | Color code (RGB) | Corine Land Cover | +|-----------|----------------------|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------|-------------------| +| 10 | Tree cover | A12A3 // A11A1 A24A3C1(C2)-R1(R2) | Geographic area dominated by trees with ≄10% cover. Other land cover types (shrubs, herbs, built-up areas, water bodies) may occur below the canopy. Includes plantations and afforestation areas (e.g., oil palm, olive trees), as well as tree-covered floodplains except mangroves. | 0, 100, 0 | | +| 20 | Shrubland | A12A4 // A11A2 | Area dominated by natural shrubs (woody perennials <5 m tall) with ≄10% cover. Trees may be scattered (<10% cover). Herbs can be present at any density. Includes both evergreen and deciduous shrubs. | 255, 187, 34 | | +| 30 | Grassland | A12A2 | Area dominated by natural herbaceous plants (grasses, prairies, steppes, savannahs, pastures) with ≄10% cover. May include grazed or fire-managed lands and uncultivated cropland without bare periods. Trees/shrubs cover <10%. | 255, 255, 76 | | +| 40 | Cropland | A11A3(A4)(A5) // A23 | Land covered by annual crops sown/planted and harvestable at least once per year. Produces herbaceous cover, possibly mixed with woody vegetation. Perennial woody crops are classified as tree/shrub cover. Greenhouses are classified as built-up. | 240, 150, 255 | | +| 50 | Built-up | B15A1 | Land covered by buildings, roads, and man-made structures (residential, industrial, railways). Urban green areas (parks, sports facilities) excluded. Waste dumps and extraction sites are ā€œbareā€. | 250, 0, 0 | | +| 60 | Bare/sparse vegetation | B16A1(A2) // B15A2 | Land with exposed soil, sand, or rocks, having ≤10% vegetation cover during any time of year. | 180, 180, 180 | | +| 70 | Snow and Ice | B28A2(A3) | Areas persistently covered by snow or glaciers. | 240, 240, 240 | | +| 80 | Permanent water bodies | B28A1(B1) // B27A1(B1) | Areas covered ≄9 months/year by lakes, reservoirs, or rivers (fresh or salt water). Water may freeze for <9 months/year. | 0, 100, 200 | | +| 90 | Herbaceous wetland | A24A2 | Land dominated by natural herbaceous vegetation (≄10% cover) permanently or regularly flooded by fresh, brackish, or salt water. Excludes unvegetated sediments, swamp forests, and mangroves. | 0, 150, 160 | | +| 95 | Mangroves | A24A3C5-R3 | Salt-tolerant trees and plants thriving in intertidal tropical zones, overwash islands, and estuaries. | 0, 207, 117 | | +| 100 | Moss and lichen | A12A7 | Land covered by lichens and/or mosses. Lichens are symbiotic fungi–algae organisms. Mosses are photoautotrophic plants without true leaves, stems, or roots. | 250, 230, 160 | | + + +## CORINE Land Cover (CLC) Classes (10.2909/960998c1-1870-4e82-8051-6485205ebbac) + +| CLC code | Land Cover Class | Description | +|----------|--------------------------------------------|----------------------------------------------------------------------------------------------| +| 111 | Continuous urban fabric | Built-up areas where buildings, roads, and artificial surfaces cover most of the ground | +| 112 | Discontinuous urban fabric | Built-up areas with significant green areas and vegetation between structures | +| 121 | Industrial or commercial units | Industrial, commercial, public, military, and private service areas | +| 122 | Road and rail networks and associated land | Motorways, railways, and associated installations | +| 123 | Port areas | Infrastructure of ports and harbors | +| 124 | Airports | Airport infrastructure | +| 131 | Mineral extraction sites | Areas of mineral extraction (quarries, open-cast mines) | +| 132 | Dump sites | Landfills and waste disposal areas | +| 133 | Construction sites | Areas under construction | +| 141 | Green urban areas | Parks, gardens, and green spaces in urban areas | +| 142 | Sport and leisure facilities | Stadiums, sports fields, golf courses, and leisure facilities | +| 211 | Non-irrigated arable land | Cropland not irrigated | +| 212 | Permanently irrigated land | Cropland with permanent irrigation | +| 213 | Rice fields | Areas devoted to rice cultivation | +| 221 | Vineyards | Land covered by vineyards | +| 222 | Fruit trees and berry plantations | Orchards, berry plantations | +| 223 | Olive groves | Land covered by olive trees | +| 231 | Pastures | Grassland used for grazing | +| 241 | Annual crops associated with permanent crops | Areas with annual and permanent crops mixed | +| 242 | Complex cultivation patterns | Small parcels with a complex pattern of fields | +| 243 | Land principally occupied by agriculture, with significant areas of natural vegetation | Mixed agricultural/natural vegetation areas | +| 244 | Agro-forestry areas | Areas with both agriculture and forestry | +| 311 | Broad-leaved forest | Forests dominated by broad-leaved trees | +| 312 | Coniferous forest | Forests dominated by coniferous trees | +| 313 | Mixed forest | Forests with both broad-leaved and coniferous trees | +| 321 | Natural grasslands | Non-agricultural grasslands | +| 322 | Moors and heathland | Shrub-dominated land (heath, moor, etc.) | +| 323 | Sclerophyllous vegetation | Shrubland with hard-leaved, drought-resistant plants | +| 324 | Transitional woodland-shrub | Areas in transition between forest and shrub or herbaceous vegetation | +| 331 | Beaches, dunes, sands | Coastal and inland sands, dunes, and beaches | +| 332 | Bare rocks | Exposed rock surfaces | +| 333 | Sparsely vegetated areas | Areas with sparse vegetation | +| 334 | Burnt areas | Areas affected by recent fire | +| 335 | Glaciers and perpetual snow | Areas covered by glaciers or permanent snow | +| 411 | Inland marshes | Wetlands with mostly fresh water, not under forest | +| 412 | Peat bogs | Wetlands with peat accumulation | +| 421 | Salt marshes | Coastal marshes with salt-tolerant vegetation | +| 422 | Salines | Saline soils or salt extraction areas | +| 423 | Intertidal flats | Areas alternately submerged and exposed by tides | +| 511 | Water courses | Rivers, streams, and canals | +| 512 | Water bodies | Lakes, reservoirs, and ponds | +| 521 | Coastal lagoons | Shallow coastal water bodies separated from the sea | +| 522 | Estuaries | Tidal mouth of a river | +| 523 | Sea and ocean | Marine waters | + + +## S2GLC Classes (Malinowski, R., Lewiński, S., Rybicki, M., Gromny, E., Jenerowicz, M., Krupiński, M., Nowakowski, A., Wojtkowski, C., Krupiński, M., KrƤtzschmar, E., Schauer, P. (2020), Automated Production of a Land Cover/Use Map of Europe Based on Sentinel-2 Imagery +doi:10.3390/rs12213523) + +| S2GLC code | Land Cover Class | Description | +|------------|--------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| +| 111 | Artificial surfaces and constructions | Surfaces altered by human construction activities, replacing natural surfaces with artificial materials. | +| 121 | Natural material surfaces | Natural consolidated or unconsolidated material surfaces (rock, sand, gravel, dunes, quarries, etc.). | +| 211 | Broadleaf tree cover | Land covered by broadleaved tree canopy that loses leaves seasonally. | +| 212 | Coniferous tree cover | Land covered by needle-leaved tree canopy that does not lose needles seasonally. | +| 221 | Herbaceous vegetation | Land covered by herbaceous vegetation, including both natural grasslands and managed grazing/mowing lands. | +| 222 | Moors and heathland | Low shrub-dominated vegetation with closed cover; limited herbaceous species allowed. | +| 223 | Sclerophyllous vegetation | Mediterranean-type bushy vegetation (maquis, garrigue), closed or discontinuous. | +| 311 | Cultivated areas | Cultivated land managed by humans, including irrigated, non-irrigated, and rice fields; includes temporary bare soils. | +| 312 | Vineyards | Areas planted with vines. | +| 411 | Marshes | Low-lying vegetated wetlands (herbaceous) with permanent or temporary water. | +| 412 | Peatbogs | Peatlands with organic deposits of decomposed vegetation, including exploited peatlands. | +| 511 | Water bodies | Water surfaces (fresh, brackish, or saline) regardless of location or origin. | +| 611 | Permanent snow covered surfaces | Areas with snow or ice cover persisting throughout the year above the climatic snow line. | +| 711 | Surfaces permanently covered by clouds| Areas obscured by persistent cloud cover or haze, where classification is not possible. | + + diff --git a/README.md b/README.md index c6ac52fb..c84db4a7 100644 --- a/README.md +++ b/README.md @@ -66,12 +66,12 @@ The easiest way to get started is often using Docker and the interactive console docker build . -t forefire:latest ``` -5. Run the container interactively +3. Run the container interactively ```bash docker run -it --rm -p 8000:8000 --name ff_interactive forefire ``` -6. Inside the container navigate to test directory and launch the forefire console: +4. Inside the container navigate to test directory and launch the forefire console: ```bash cd tests/runff @@ -80,9 +80,10 @@ The easiest way to get started is often using Docker and the interactive console ``` -67 ```bash +5. Launch the HTTP server from the console: + ```bash listenHTTP[] - ```` + ``` the output should be : ```bash @@ -91,15 +92,15 @@ The easiest way to get started is often using Docker and the interactive console This server provides a graphical user interface that you can access on your browser at http://localhost:8000/ -7. Run your first simulation +6. Run your first simulation In ForeFire (web or on console are equivalent), running a simulation and viewing the result are separate commands. The UI guides you through this process. - **Step 1: Run the simulation script.** In HTTP Interface click the **`include`** button or type `include[real_case.ff]` in command input box, and click the **`Send`** button. - ou can also run the same command directly in the interactive console if you prefer py typing `include[real_case.ff]` and press enter. - The scripts executes a simulation by loading data, starting fires, applying wind triggers, and running the simulation for a specified duration. + You can also run the same command directly in the interactive console if you prefer, by typing `include[real_case.ff]` and pressing enter. + The script executes a simulation by loading data, starting fires, applying wind triggers, and running the simulation for a specified duration. - **Step 2: View the result.** After the command finishes, click the **`Refresh Map`** button to display the simulation results onto the web map. - - **Step 3 (optional): iterate more.** you can continue the simulation, command `include[real_case.ff]` the **`Refresh Map`** button to display the simulation results onto the web map. + - **Step 3 (optional): iterate more.** You can continue the simulation by running the `include[real_case.ff]` command again and clicking the **`Refresh Map`** button to display the updated simulation results onto the web map. ![ForeFire Web UI showing a simulation example](docs/source/_static/images/gui_real_case_ff.jpg) diff --git a/src/BurningMapLayer.h b/src/BurningMapLayer.h index 76bbf969..c12c9a10 100644 --- a/src/BurningMapLayer.h +++ b/src/BurningMapLayer.h @@ -130,7 +130,7 @@ T BurningMapLayer::getNearestData(FFPoint loc){ template void BurningMapLayer::getMatrix( FFArray** matrix, const double& t){ - cout << "getting matrix lmayer "<* myLayer = session->fd->getDataLayer(tmpname); - + if ( myLayer ){ + /* + int mprank = SimulationParameters::GetInstance()->getInt("mpirank"); + if (mprank ==1 ){ + cout<* myMatrix; // getting the pointer myLayer->getMatrix(&myMatrix, executor.getTime()); @@ -682,13 +688,6 @@ void FFPutDoubleArray(const char* mname, double* x, MPI_Comm_rank(MPI_COMM_WORLD, &world_rank); MPI_Comm_size(MPI_COMM_WORLD, &world_size); - if (tmpname == "BRatio"){ - double sumBR = myMatrix->sum(); - if (sumBR > 0.0){ - // cout << world_rank <<" has "<< myMatrix->sum()<<" size M " <getSize()<<" sizein "<< sizein << " sizeout "<< sizeout << endl; - } - - } #endif myMatrix->copyDataToFortran(x); @@ -721,10 +720,7 @@ void FFDumpDoubleArray(size_t nmodel, size_t nip, const char* mname, double t try { -// size_t indF = 0; -// for ( indF = 0; indF < sizein; indF++ ) { -// FileOut.write(reinterpret_cast(x+indF), sizeof(double)); -// } + FileOut.write(reinterpret_cast(x), sizein * sizeof(double)); } catch (...) { diff --git a/src/Command.cpp b/src/Command.cpp index 1f45ae8b..d2509161 100644 --- a/src/Command.cpp +++ b/src/Command.cpp @@ -1663,17 +1663,14 @@ namespace libforefire } }else{ *currentSession.outStream << kmlStream.str() << std::endl; - } - + } } - - - } - } + } // -------------------------------------------------------------- // ELSE: original logic for other parameters / file extensions // -------------------------------------------------------------- + else { // Standard approach: fetch single matrix for the requested "parameter" @@ -1865,7 +1862,6 @@ namespace libforefire // 'modelName' is optional (used for flux layers) std::string modelName = ""; - cout << "Adding layer: " << layerName << " of type: " << layerType << "Model: " << modelName << " ARG "< double + { + if (iso == stringError) + return FLOATERROR; + SimulationParameters *simParam = SimulationParameters::GetInstance(); + int year = 0, yday = 0; + double secs = 0.; + if (!simParam->ISODateDecomposition(iso, secs, year, yday)) + { + return FLOATERROR; + } + return simParam->SecsBetween(simParam->getDouble("refTime"), + simParam->getInt("refYear"), + simParam->getInt("refDay"), + secs, year, yday); + }; + + auto parsePointString = [&](const string &raw, bool lonlat) -> FFPoint + { + if (raw == stringError || raw.size() < 2) + return pointError; + string inner = raw.substr(1, raw.size() - 2); + vector tokens; + tokenize(inner, tokens, ","); + if (tokens.size() != 3) + return pointError; + double a, b, c; + if (!(istringstream(tokens[0]) >> a && istringstream(tokens[1]) >> b && istringstream(tokens[2]) >> c)) + { + return pointError; + } + if (lonlat) + { + double x = domain->getXFromLon(a); + double y = domain->getYFromLat(b); + return FFPoint(x, y, c); + } + return FFPoint(a, b, c); + }; + + string layer = getString("layer", arg); + if (layer == stringError) + layer = getString("name", arg); + if (layer == stringError) + { + cout << "emit: missing layer parameter (layer=...)" << endl; + return error; + } + + + // Spatial support + constexpr double pi = 3.14159265358979323846; + double radius = getFloat("radius", arg); + double surface = getFloat("surface", arg); + if (surface == FLOATERROR) + surface = getFloat("area", arg); + + FFPoint center = pointError; + + FFPoint loc = getPoint("loc", arg); + if (loc != pointError) + center = loc; + FFPoint lonlat = getPoint("lonlat", arg); + if (lonlat != pointError) + center = lonlat; + + FFPoint sw = getPoint("sw", arg); + FFPoint ne = getPoint("ne", arg); + FFPoint swlonlat = parsePointString(getString("swlonlat", arg), true); + FFPoint nelonlat = parsePointString(getString("nelonlat", arg), true); + + if (sw != pointError && ne != pointError) + { + center = FFPoint(0.5 * (sw.x + ne.x), 0.5 * (sw.y + ne.y), 0.5 * (sw.z + ne.z)); + double width = fabs(ne.x - sw.x); + double height = fabs(ne.y - sw.y); + if (surface == FLOATERROR) + surface = width * height; + } + else if (swlonlat != pointError && nelonlat != pointError) + { + center = FFPoint(0.5 * (swlonlat.x + nelonlat.x), 0.5 * (swlonlat.y + nelonlat.y), 0.5 * (swlonlat.z + nelonlat.z)); + double width = fabs(nelonlat.x - swlonlat.x); + double height = fabs(nelonlat.y - swlonlat.y); + if (surface == FLOATERROR) + surface = width * height; + } + + if (radius != FLOATERROR && surface == FLOATERROR) + { + surface = pi * radius * radius; + } + + if (surface == FLOATERROR) + { + surface = 0.0; + } + + // Time window: start now, duration required + double tStart = domain->getTime(); + double duration = getFloat("duration", arg); + if (duration == FLOATERROR) + { + cout << "emit: missing duration=... (seconds)" << endl; + return error; + } + if (duration < 0) + { + cout << "emit: duration must be non-negative." << endl; + return error; + } + double tEnd = tStart + duration; + + // Flux handling with optional FRP (MW) conversion + double flux = getFloat("flux", arg); + double frpMw = getFloat("frp", arg); + double value = FLOATERROR; + double total = FLOATERROR; + + if (flux == FLOATERROR) + { + if (frpMw != FLOATERROR) + { + if (surface <= 0.0 && radius == FLOATERROR) + { + cout << "emit: FRP given but area/radius missing or zero." << endl; + return error; + } + if (surface <= 0.0 && radius != FLOATERROR) + { + surface = pi * radius * radius; + } + if (surface <= 0.0) + { + cout << "emit: FRP requires a positive area." << endl; + return error; + } + double frptoHeatWatt = currentSession.params->getDouble("FRPToWatts"); + if (frptoHeatWatt == FLOATERROR || frptoHeatWatt <= 0.0) { frptoHeatWatt = 1e7; }// default MW->W + double watts = frpMw * frptoHeatWatt ; // radiant MW -> W, then to total heat + flux = watts / surface; + } + else + { + value = getFloat("value", arg); + if (value == FLOATERROR) + value = getFloat("val", arg); + total = getFloat("total", arg); + + if (value != FLOATERROR) + { + if (surface <= 0.0) + { + cout << "emit: value given but area/surface missing or zero." << endl; + return error; + } + flux = value / surface; + } + else if (total != FLOATERROR) + { + if (surface <= 0.0 || duration <= 0.0) + { + cout << "emit: total given but duration or area is zero." << endl; + return error; + } + flux = total / (surface * duration); + } + } + } + + if (center == pointError) + { + cout << "emit: no location provided (use loc=(), lonlat=(), sw+ne)." << endl; + return error; + } + + if (flux == FLOATERROR) + { + cout << "emit: provide flux=..., FRP=..., value=... (power), or total=... (energy)." << endl; + return error; + } + + bool ok = domain->emitFlux(layer, center, surface, tStart, tEnd, flux); + if (!ok) + { + cout << "emit: domain failed to register emission request." << endl; + return error; + } + return normal; + } + void Command::executeLoop(ifstream* inputStream) { // Support multiline commands whose arguments are wrapped in triple quotes ("""). @@ -3349,24 +3544,22 @@ namespace libforefire int colorIndex = 0; double range = maxVal - minVal; - - if (std::isfinite(val) && std::isfinite(minVal) && std::isfinite(maxVal) && range != 0.0) { - double normalized = (val - minVal) / range; - colorIndex = static_cast(normalized * (mapSize - 1)); - colorIndex = std::max(0, std::min(colorIndex, mapSize - 1)); + if (std::isfinite(val) && std::isfinite(minVal) && std::isfinite(maxVal) && range > 0.0 && mapSize > 0) { + double normalized = 0.0; + if (val <= minVal) { + normalized = 0.0; + } else if (val >= maxVal) { + normalized = 1.0; + } else { + normalized = (val - minVal) / range; // safe: numerator smaller than range + } + if (!std::isfinite(normalized)) normalized = 0.0; + colorIndex = static_cast(normalized * static_cast(mapSize - 1)); + colorIndex = std::max(0, std::min(colorIndex, static_cast(mapSize - 1))); } else { // Failsafe: set to 0 or 1 depending on mapSize - colorIndex = (mapSize > 1) ? 1 : 0; - } - - //int colorIndex = static_cast((val - minVal) / (maxVal - minVal) * (mapSize - 1)); - // colorIndex = std::max(0, std::min(colorIndex, mapSize - 1)); - /* if ((x % 1000 == 0) && (y % 1000 == 0)) { - std::cout << val << " : " << colorIndex << std::endl; - }*/ - /* if ((x % 1000 == 0) && (y % 1000 == 0)) { - std::cout << val << " : " << colorIndex << std::endl; - }*/ + colorIndex = (mapSize > 1) ? 1 : 0; + } const auto &color = colorMap[colorIndex]; image[index] = color[0]; image[index + 1] = color[1]; diff --git a/src/Command.h b/src/Command.h index 2a55882d..bd0f8b36 100644 --- a/src/Command.h +++ b/src/Command.h @@ -72,7 +72,7 @@ class Command { // Definition of the command map alias typedef int (*cmd)(const string&, size_t&); typedef map commandMap; /*!< map of aliases between strings and functions to be called */ - static const int numberCommands = 21; /*!< number of possible commands */ + static const int numberCommands = 22; /*!< number of possible commands */ static commandMap makeCmds(){ // Construction of the command translator commandMap trans; @@ -97,6 +97,7 @@ class Command { trans["listenHTTP"] = &listenHTTP; trans["clear"] = &clear; trans["quit"] = &quit; + trans["emit"] = &emit; return trans; } @@ -179,6 +180,8 @@ class Command { static int quit(const string&, size_t&); /*! \brief command to quit the ForeFire shell */ static int listenHTTP(const string&, size_t &) ; + /*! \brief command to emit a flux over a given area for a time span */ + static int emit(const string&, size_t&); static string executeCommandAndCaptureOutput(const std::string &cmd); diff --git a/src/DataBroker.cpp b/src/DataBroker.cpp index 63ef34e9..5a70fd00 100644 --- a/src/DataBroker.cpp +++ b/src/DataBroker.cpp @@ -721,12 +721,11 @@ namespace libforefire // Iterate over regular layers and add their names. for (const auto &entry : layersMap) { - - names.push_back(entry.first); + names.push_back(entry.first); } // Iterate over flux layers and add their names. - + return names; } @@ -768,21 +767,7 @@ namespace libforefire return 0; } - void DataBroker::computeActiveSurfacesFlux(const double &t) - { - // Scanning the scalar layers - map *>::iterator iter = fluxLayersMap.begin(); - int numFluxModelsMax = 10; - for (; iter != fluxLayersMap.end(); ++iter) - { - FluxLayer *flayer = iter->second; - int modelCount[numFluxModelsMax]; - for (int i = 0; i < numFluxModelsMax; i++) - modelCount[i] = 0; - flayer->computeActiveMatrix(t, modelCount); - } - } /* *************************************** */ /* Property getters for propagation models */ diff --git a/src/DataBroker.h b/src/DataBroker.h index 0b7342e6..d5901a38 100644 --- a/src/DataBroker.h +++ b/src/DataBroker.h @@ -284,8 +284,6 @@ class DataBroker { /*! \brief accessor to the desired flux layer */ FluxLayer* getFluxLayer(const string&); - /*! \brief recomputes flux actives surfaces for each model */ - void computeActiveSurfacesFlux(const double&); void loadMultiWindBin(double , size_t , size_t* , size_t* ); /*! \brief accessor to the desired set of properties for propagation models */ diff --git a/src/FDCell.cpp b/src/FDCell.cpp index 9fc281cd..b36a0041 100644 --- a/src/FDCell.cpp +++ b/src/FDCell.cpp @@ -180,12 +180,15 @@ void FDCell::removeFireNode(FireNode* fn){ double FDCell::getBurningRatio(const double& t){ /* if the burning map is not allocated */ - if ( arrivalTimes == 0 ) return 0.; + + //if ( arrivalTimes == 0 ) return 0.; + /* else getting the ratio and checking * that there is still something burning*/ double numBurningCells = 0; FFPoint center; center.setX(SWCorner.getX()+0.5*dx); + for ( size_t i=0; i= at ){ - int mind = dataBroker->heatFluxLayer->getFunctionIndexAt(loc, t); + // double at = getArrivalTime(loc); + // if ( t >= at ){ + //int mind = dataBroker->heatFluxLayer->getFunctionIndexAt(loc, t); - double heatFlux = getModelValueAt(mind, loc, t, t, at); + //double heatFlux = getModelValueAt(mind, loc, t, t, at); + double heatFlux = dataBroker->heatFluxLayer->getValueAt( loc, t); if ( heatFlux > burningTresholdFlux ) { return true; } - } + //} return false; } @@ -3366,12 +3368,12 @@ resX = extractWidth/eni; resY = extractHeight/enj; } - FFPoint itp = FFPoint(SWbound.getX()+resX/2, SWbound.getY()+resY/2, 0.0); + FFPoint itp = FFPoint(SWbound.getX()+resX/2, SWbound.getY()-resY, 0.0); std::vector> matrix(eni, std::vector(enj, -9999)); for (size_t i = 0; i < eni; i++) { itp.setX(SWbound.getX()+resX/2 + i*resX); for (size_t j = 0; j < enj; j++) { - itp.setY(SWbound.getY()+resY/2 + j*resY); + itp.setY(SWbound.getY()-resY + j*resY); matrix[i][j] = dataLayer->getValueAt(itp,lTime); } } @@ -3597,6 +3599,22 @@ } cout<* fl = getFluxLayer(layerName); + if (fl == nullptr) { + cout << "emitFlux: flux layer '" << layerName << "' not found." << endl; + return false; + } + fl->addEmission(center, surfaceArea, startTime, endTime, value); + /* cout << "emitFlux registered -> layer: " << layerName + << " center: (" << center.x << "," << center.y << "," << center.z << ")" + << " area: " << surfaceArea + << " start: " << startTime + << " end: " << endTime + << " flux: " << value << endl;*/ + return true; + } + } - \ No newline at end of file + diff --git a/src/FireDomain.h b/src/FireDomain.h index e79f4097..3debc093 100644 --- a/src/FireDomain.h +++ b/src/FireDomain.h @@ -631,6 +631,8 @@ class FireDomain: public ForeFireAtom, Visitable { std::vector> getDataMatrix(const std::string& , FFPoint& , FFPoint& , size_t& , size_t& ) ; /*! \brief checking the burning status of a given location */ bool checkForBurningStatus(FFPoint&); + /*! \brief register an emission request on a given flux layer (stub) */ + bool emitFlux(const std::string& layerName, const FFPoint& center, double surfaceArea, double startTime, double endTime, double value); }; diff --git a/src/FireFront.cpp b/src/FireFront.cpp index d11f8795..e292708d 100644 --- a/src/FireFront.cpp +++ b/src/FireFront.cpp @@ -314,7 +314,7 @@ void FireFront::splineInterp(FireNode* ifn, FFVector& nml, double& kappa){ d2x = new double[nspl]; if ( d2y!=0 ) delete [] d2y; d2y = new double[nspl]; - if ( u!=0 ) delete [] z; + if ( u!=0 ) delete [] u; u = new double[nspl]; if ( z!=0 ) delete [] z; z = new double[nspl]; diff --git a/src/FluxLayer.h b/src/FluxLayer.h index c974c3a4..5491b220 100644 --- a/src/FluxLayer.h +++ b/src/FluxLayer.h @@ -13,6 +13,9 @@ #include "FDCell.h" #include "FFArrays.h" #include "DataBroker.h" +#include +#include +#include using namespace std; @@ -64,8 +67,16 @@ template class FluxLayer : public DataLayer { double mapDt; /*!< increment in the T direction */ double latestCallGetMatrix; /*!< time of the latest call to getMatrix() */ - double latestCallInstantaneousFlux; /*!< time of the latest call to getInstantaneousFlux() */ - + + struct Emission { + FFPoint center; + double area; + double radius; + double startTime; + double endTime; + double flux; // per-unit-area + }; + std::vector emissions; SimulationParameters* params; @@ -73,6 +84,8 @@ template class FluxLayer : public DataLayer { size_t getPosInMap(FFPoint&, const double&); T getNearestData(FFPoint, double); + void pruneEmissions(const double& t); + double emissionContributionAt(const double& x, const double& y, const double& t) const; public: /*! \brief Default constructor */ @@ -106,7 +119,6 @@ template class FluxLayer : public DataLayer { mapDt = 1; latestCallGetMatrix = -1.; - latestCallInstantaneousFlux = -1.; params = SimulationParameters::GetInstance(); } @@ -141,7 +153,6 @@ template class FluxLayer : public DataLayer { mapDt = timespan/mapNt; latestCallGetMatrix = -1.; - latestCallInstantaneousFlux = -1.; params = SimulationParameters::GetInstance(); }; @@ -166,14 +177,11 @@ template class FluxLayer : public DataLayer { void setFirstCall(const double&); /*! \brief getter to the pointer on the FFArray */ void getMatrix(FFArray**, const double&); + /*! \brief register a timed emission */ + void addEmission(const FFPoint& center, double area, double startTime, double endTime, double fluxPerArea); /*! \brief stores data from a fortran array into the FFArray */ void setMatrix(string&, double*, const size_t&, size_t&, const double&); - /*! \brief getter to the pointer on the FFArray */ - void getInstantaneousFlux(FFArray**, const double&); - - /*! \brief fill an aray with active bmap cells */ - int computeActiveMatrix(const double& , int*); /*! \brief print the related FFArray */ string print2D(size_t, size_t); @@ -238,6 +246,43 @@ T FluxLayer::getValueAt(FFPoint loc, const double& t){ return getNearestData(loc, t); } +template +void FluxLayer::pruneEmissions(const double& t){ + if (emissions.empty()) return; + emissions.erase( + std::remove_if(emissions.begin(), emissions.end(), [&](const Emission& e){ + return e.endTime < t; + }), + emissions.end()); +} + +template +double FluxLayer::emissionContributionAt(const double& x, const double& y, const double& t) const { + if (emissions.empty()) return 0.0; + const double halfDx = dx * 0.5; + const double halfDy = dy * 0.5; + for (const auto& e : emissions) { + + if (t < e.startTime || t > e.endTime) { + continue; + } + double dxp = x - e.center.x; + double dyp = y - e.center.y; + double dist2 = dxp*dxp + dyp*dyp; + if (e.radius > 0.0) { + if (dist2 <= e.radius * e.radius) { + + return e.flux; + } + } else { + if (std::fabs(dxp) <= halfDx && std::fabs(dyp) <= halfDy) { + return e.flux; + } + } + } + return 0.0; +} + template T FluxLayer::getNearestData(FFPoint loc, double t){ if ( loc.getX() < SWCorner.getX() or loc.getX() > NECorner.getX() ){ @@ -253,12 +298,17 @@ T FluxLayer::getNearestData(FFPoint loc, double t){ nx > 1 ? i = ((size_t) (loc.getX()-SWCorner.getX())/dx) : i = 0; ny > 1 ? j = ((size_t) (loc.getY()-SWCorner.getY())/dy) : j = 0; - + int numFluxModelsMax = 50; int modelCount[numFluxModelsMax]; for (int i = 0; i < numFluxModelsMax; i++) modelCount[i]= 0; double v= cells[i][j].applyModelsOnBmap(this->getKey(), t, t, modelCount); - + pruneEmissions(t); + double cx = SWCorner.getX() + ( (double)i + 0.5 ) * dx; + double cy = SWCorner.getY() + ( (double)j + 0.5 ) * dy; + + v += emissionContributionAt(cx, cy, t); + return v; } @@ -284,54 +334,50 @@ void FluxLayer::setFirstCall(const double& t){ } template -int FluxLayer::computeActiveMatrix(const double& t, int* modelCount){ - int totalCount = 0; - string fluxName = this->getKey(); - for ( size_t i = 0; i < nx; i++ ){ - for ( size_t j = 0; j < ny; j++ ){ - totalCount += cells[i][j].activeModelsOnBmap(fluxName, t, modelCount); - } - } - return totalCount; +void FluxLayer::addEmission(const FFPoint& center, double area, double startTime, double endTime, double fluxPerArea){ + Emission e; + e.center = center; + e.area = area; + e.radius = (area > 0.0) ? std::sqrt(area / 3.14159265358979323846) : 0.0; + e.startTime = startTime; + e.endTime = endTime; + e.flux = fluxPerArea; + emissions.push_back(e); } + + template void FluxLayer::getMatrix(FFArray** matrix, const double& t){ + int domID = params->getInt("mpirank"); - if ( t != latestCallGetMatrix ){ + if ( t != latestCallGetMatrix ){ // TODO computing active area string fluxName = this->getKey(); int numFluxModelsMax = 50; int modelCount[numFluxModelsMax]; - - for (int i = 0; i < numFluxModelsMax; i++) modelCount[i]= 0; - int totalcount = 0; - - - for ( size_t i = 0; i < nx; i++ ){ - for ( size_t j = 0; j < ny; j++ ){ - (*flux)(i,j) = cells[i][j].applyModelsOnBmap(fluxName, latestCallGetMatrix, t, modelCount); - } - } - - for (int i = 0; i < numFluxModelsMax; i++) { - string modelName=cells[0][0].getFluxModelName(i); - if (!modelName.empty()){ - params->setDouble(modelName+".activeArea",modelCount[i]*cells[0][0].getBmapElementArea()); - totalcount += modelCount[i]; - } - } - - for ( size_t i = 0; i < nx; i++ ){ + for (int i = 0; i < numFluxModelsMax; i++) modelCount[i]= 0; + pruneEmissions(t); + + + double sumAdded = 0.0; + for ( size_t i = 0; i < nx; i++ ){ for ( size_t j = 0; j < ny; j++ ){ - (*flux)(i,j) = cells[i][j].applyModelsOnBmap(fluxName, latestCallGetMatrix, t, modelCount); + double base = cells[i][j].applyModelsOnBmap(fluxName, latestCallGetMatrix, t, modelCount); + double cx = SWCorner.getX() + ( (double)i + 0.5 ) * dx; + double cy = SWCorner.getY() + ( (double)j + 0.5 ) * dy; + double added = emissionContributionAt(cx, cy, t); + (*flux)(i,j) = base + added; + sumAdded += (added+base); } } + if ((domID == 1 ) && (fluxName == "heatFlux" )){ + double diffN = t - latestCallGetMatrix ; + //cout<<"ID "<getKey() <<" has "<setDouble(fluxName+".activeArea",totalcount*cells[0][0].getBmapElementArea()); latestCallGetMatrix = t; - } // Affecting the computed matrix to the desired array *matrix = flux; @@ -344,37 +390,7 @@ void FluxLayer::getMatrix(FFArray** matrix, const double& t){ } } -template -void FluxLayer::getInstantaneousFlux(FFArray** matrix, const double& t){ - string fluxName = this->getKey(); - int numFluxModelsMax = 50; - int modelCount[numFluxModelsMax]; - int totalcount = 0; - for (int i = 0; i < numFluxModelsMax; i++) modelCount[i]= 0; - if ( t != latestCallInstantaneousFlux ){ - // computing the instantaneous flux - string fluxName = this->getKey(); - for ( size_t i = 0; i < nx; i++ ){ - for ( size_t j = 0; j < ny; j++ ){ - (*flux)(i,j) = cells[i][j].applyModelsOnBmap(fluxName, t, t, modelCount); - } - } - - for (int i = 0; i < numFluxModelsMax; i++) { - string modelName=cells[0][0].getFluxModelName(i); - if (!modelName.empty()){ - params->setDouble(modelName+".activeArea",modelCount[i]*cells[0][0].getBmapElementArea()); - totalcount += modelCount[i]; - } - } - params->setDouble(fluxName+".activeArea",totalcount*cells[0][0].getBmapElementArea()); - - latestCallInstantaneousFlux = t; - } - // Affecting the computed matrix to the desired array - *matrix = flux; -} template void FluxLayer::setMatrix(string& mname, double* inMatrix diff --git a/src/SimulationParameters.cpp b/src/SimulationParameters.cpp index 899eaeaf..d01a3be9 100644 --- a/src/SimulationParameters.cpp +++ b/src/SimulationParameters.cpp @@ -366,6 +366,8 @@ std::string landCoverIndexedCSV = R"(Index;Rhod;Rhol;Md;Ml;sd;sl;e;Sigmad;Sigmal parameters.insert(make_pair("normalScheme","medians")); parameters.insert(make_pair("curvatureComputation", "1")); parameters.insert(make_pair("curvatureScheme","circumradius")); + + parameters.insert(make_pair("FRPToWatts","10000000")); parameters.insert(make_pair("frontDepthComputation", "0")); parameters.insert(make_pair("frontDepthScheme","normalDir")); parameters.insert(make_pair("minimalPropagativeFrontDepth","10.")); @@ -424,6 +426,7 @@ std::string landCoverIndexedCSV = R"(Index;Rhod;Rhol;Md;Ml;sd;sl;e;Sigmad;Sigmal parameters.insert(make_pair("max_inner_front_nodes_filter","50")); parameters.insert(make_pair("heatFluxDefaultModel","heatFluxBasic")); parameters.insert(make_pair("vaporFluxDefaultModel","vaporFluxBasic")); + parameters.insert(make_pair("SPOTF1DefaultModel","SpottingFluxBasic")); } diff --git a/src/flux/CraterSO2FluxModel.cpp b/src/flux/CraterSO2FluxModel.cpp deleted file mode 100644 index 1b53fd03..00000000 --- a/src/flux/CraterSO2FluxModel.cpp +++ /dev/null @@ -1,141 +0,0 @@ -/** - * @file CraterSO2FluxModel.cpp - * @brief SO2 flux model for a Volcano Crater (Durand PhD thesis). - * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. - * @license This program is free software; See LICENSE file for details. (See LICENSE file). - * @author Jean‑Baptiste Filippi — 2025 - */ - -#include "../FluxModel.h" -#include "../FireDomain.h" -#include "../include/Futils.h" -using namespace std; -namespace libforefire { - -class CraterSO2FluxModel: public FluxModel { - - /*! name the model */ - static const string name; - - /*! boolean for initialization */ - static int isInitialized; - - /*! properties needed by the model */ - - /*! coefficients needed by the model */ - double eruptionTime; - double craterArea; - vector refHours; - vector refFlows; - double emissionRatio; - - /*! local variables */ - double convert; - - /*! result of the model */ - double getValue(double*, const double& - , const double&, const double&); - -public: - CraterSO2FluxModel(const int& = 0, DataBroker* = 0); - virtual ~CraterSO2FluxModel(); - - string getName(); -}; - -FluxModel* getCraterSO2FluxModel(const int& = 0, DataBroker* = 0); - - -/* name of the model */ -const string CraterSO2FluxModel::name = "CraterSO2Flux"; - -/* instantiation */ -FluxModel* getCraterSO2FluxModel(const int& index, DataBroker* db) { - return new CraterSO2FluxModel(index, db); -} - -/* registration */ -int CraterSO2FluxModel::isInitialized = - FireDomain::registerFluxModelInstantiator(name, getCraterSO2FluxModel ); - -/* constructor */ -CraterSO2FluxModel::CraterSO2FluxModel( - const int & mindex, DataBroker* db) - : FluxModel(mindex, db) { - - /* defining the properties needed for the model */ - - /* allocating the vector for the values of these properties */ - if ( numProperties > 0 ) properties = new double[numProperties]; - - /* registering the model in the data broker */ - dataBroker->registerFluxModel(this); - - /* Definition of the coefficients */ - eruptionTime = 0.; - if ( params->isValued("lava.eruptionTime") ) - eruptionTime = params->getDouble("lava.eruptionTime"); - craterArea = 1600.; - if ( params->isValued("crater.area") ) - craterArea = params->getDouble("crater.area"); - if ( !params->isValued("SO2.hours") ) - cout<<"ERROR: vector of parameters SO2.hours should be valued"<getDoubleArray("SO2.hours"); - if ( !params->isValued("SO2.flows") ) - cout<<"ERROR: vector of parameters SO2.flows should be valued"<getDoubleArray("SO2.flows"); - emissionRatio = 1.0; - if ( params->isValued("SO2.craterRatio") ) - emissionRatio = params->getDouble("SO2.craterRatio"); - - /* local variables */ - // Error estimate correction - convert =1.; - // converting from kg.s-1 to molecules.s-1 - convert = 6.022e23*(convert/64.e-3); -} - -/* destructor (shoudn't be modified) */ -CraterSO2FluxModel::~CraterSO2FluxModel() { - if ( properties != 0 ) delete properties; -} - -/* accessor to the name of the model */ -string CraterSO2FluxModel::getName(){ - return name; -} - -/* ****************** */ -/* Model for the flux */ -/* ****************** */ - -double CraterSO2FluxModel::getValue(double* valueOf - , const double& bt, const double& et, const double& at){ - - if ( bt - eruptionTime < 0 ) return 0.; - - /* getting the hours since eruption */ - double hoursSinceEruption = (bt-eruptionTime)/3600.; - if ( hoursSinceEruption > refHours.back() ) return 0.; - - /* getting the index of vector of fluxes */ - size_t hind = 0; - size_t nhours = refHours.size(); - while ( hind+1 < nhours and refHours[hind+1] < hoursSinceEruption ) hind++; - /* interpolation of the flux between the values */ - double beta = (hoursSinceEruption-refHours[hind]) - /(refHours[hind+1]-refHours[hind]); - - double flux = beta*refFlows[hind+1] + (1.-beta)*refFlows[hind]; - double craso2 = convert*emissionRatio*flux; - //cout << " craso2 " << craso2 << " convert " << convert << " flux " << flux << endl; -/* CraterSO2Flux.activeArea = surface active en m² du cratere -// converting from molecule.s-1 to molecules.m².s-1*/ -// double t = bt-at; -// cout << " bt " << bt << " at " << at << " et " << et << endl; -// cout << "bt-at " << t << endl; -// return craso2/params->getDouble("CraterSO2Flux.activeArea"); - return craso2/931; -} - -} /* namespace libforefire */ diff --git a/src/flux/LavaCO2FluxModel.cpp b/src/flux/LavaCO2FluxModel.cpp deleted file mode 100644 index bad5d337..00000000 --- a/src/flux/LavaCO2FluxModel.cpp +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @file LavaCO2FluxModel.cpp - * @brief TODO: add a brief description. - * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. - * @license This program is free software; See LICENSE file for details. (See LICENSE file). - * @author Jean‑Baptiste Filippi — 2025 - */ - - -#include "../FluxModel.h" -#include "../FireDomain.h" -#include "../include/Futils.h" -using namespace std; -namespace libforefire { - -class LavaCO2FluxModel: public FluxModel { - - /*! name the model */ - static const string name; - - /*! boolean for initialization */ - static int isInitialized; - - /*! properties needed by the model */ - size_t BR; - /*! coefficients needed by the model */ - double eruptionTime; - double burningDuration; - - /*! local variables */ - - /*! result of the model */ - double getValue(double*, const double& - , const double&, const double&); - -public: - - LavaCO2FluxModel(const int& = 0, DataBroker* = 0); - virtual ~LavaCO2FluxModel(); - - string getName(); -}; - -FluxModel* getLavaCO2FluxModel(const int& = 0, DataBroker* = 0); - -/* name of the model */ -const string LavaCO2FluxModel::name = "LavaCO2Flux"; - -/* instantiation */ -FluxModel* getLavaCO2FluxModel(const int& index, DataBroker* db) { - return new LavaCO2FluxModel(index, db); -} - -/* registration */ -int LavaCO2FluxModel::isInitialized = - FireDomain::registerFluxModelInstantiator(name, getLavaCO2FluxModel ); - -/* constructor */ -LavaCO2FluxModel::LavaCO2FluxModel( - const int & mindex, DataBroker* db) - : FluxModel(mindex, db) { -/* defining the properties needed for the model */ - burningDuration = 1000.; - if ( params->isValued("burningDuration") ) - burningDuration = params->getDouble("burningDuration"); - eruptionTime = 0.; - if ( params->isValued("lava.eruptionTime") ) - eruptionTime = params->getDouble("lava.eruptionTime"); - -/* allocating the vector for the values of these properties */ -if ( numProperties > 0 ) properties = new double[numProperties]; -/* registering the model in the data broker */ -BR = registerProperty("BRatio"); -dataBroker->registerFluxModel(this); - -} -/* Definition of the coefficients */ - -/* destructor (shoudn't be modified) */ -LavaCO2FluxModel::~LavaCO2FluxModel() { -if ( properties != 0 ) delete properties; -} - -/* accessor to the name of the model */ -string LavaCO2FluxModel::getName(){ - return name; -} - -/* ****************** */ -/* Model for the flux */ -/* ****************** */ - -double LavaCO2FluxModel::getValue(double* valueOf - , const double& bt, const double& et, const double& at){ -// if ( bt - eruptionTime < 0 ) return 0.; -// return 93/params->getDouble("LavaCO2Flux.activeArea"); -// if ( at > bt ) return 0; -// if ( bt < at + 10000) return 2; - if ((bt-at) < 0) return 0.; -// double t = (bt-eruptionTime); -// double t=bt-at; -// double flux= 0.48*exp(-0.1*(t*exp(0.5))); - if((bt-at) < (2*3600)) return 0.013; - return 0.; -// return flux; -// return 1; -} - - -}/* namespace libforefire */ - diff --git a/src/flux/LavaHCLFluxModel.cpp b/src/flux/LavaHCLFluxModel.cpp deleted file mode 100644 index c0e49cc9..00000000 --- a/src/flux/LavaHCLFluxModel.cpp +++ /dev/null @@ -1,132 +0,0 @@ -/** - * @file LavaHCLFluxModel.cpp - * @brief TODO: add a brief description. - * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. - * @license This program is free software; See LICENSE file for details. (See LICENSE file). - * @author Jean‑Baptiste Filippi — 2025 - */ - -#include "../FluxModel.h" -#include "../FireDomain.h" -#include "../include/Futils.h" -using namespace std; -namespace libforefire { - -class LavaHCLFluxModel: public FluxModel { - - /*! name the model */ - static const string name; - - /*! boolean for initialization */ - static int isInitialized; - - /*! properties needed by the model */ - - /*! coefficients needed by the model */ - double arrivalTime; - vector refHours; - vector refFlows; - double exchangeArea; - - /*! local variables */ - double convert; - - /*! result of the model */ - double getValue(double*, const double& - , const double&, const double&); - -public: - - LavaHCLFluxModel(const int& = 0, DataBroker* = 0); - virtual ~LavaHCLFluxModel(); - - string getName(); -}; - -FluxModel* getLavaHCLFluxModel(const int& = 0, DataBroker* = 0); - - -/* name of the model */ -const string LavaHCLFluxModel::name = "LavaHCLFlux"; - -/* instantiation */ -FluxModel* getLavaHCLFluxModel(const int& index, DataBroker* db) { - return new LavaHCLFluxModel(index, db); -} - -/* registration */ -int LavaHCLFluxModel::isInitialized = - FireDomain::registerFluxModelInstantiator(name, getLavaHCLFluxModel ); - -/* constructor */ -LavaHCLFluxModel::LavaHCLFluxModel( - const int & mindex, DataBroker* db) - : FluxModel(mindex, db) { - /* defining the properties needed for the model */ - /* allocating the vector for the values of these properties */ - if ( numProperties > 0 ) properties = new double[numProperties]; - /* registering the model in the data broker */ - dataBroker->registerFluxModel(this); - - /* Definition of the coefficients */ - arrivalTime = 5600.; - if(params->isValued("vaporFlux.activeArea")) -// cout<getDouble("vaporFlux.activeArea")<isValued("laze.arrivalTime") ) - arrivalTime = params->getDouble("laze.arrivalTime"); - if ( !params->isValued("laze.hours") ) - cout<<"ERROR: vector of parameters laze.hours should be valued"<getDoubleArray("laze.hours"); - if ( !params->isValued("laze.flows") ) - cout<<"ERROR: vector of parameters laze.flows should be valued"<getDoubleArray("laze.flows"); - exchangeArea = 80000.; - if ( params->isValued("laze.exchangeArea") ) - exchangeArea = params->getDouble("laze.exchangeArea"); - /* coefficients */ - // Dividing by the final area of exchange to convert to kg.m-2.s-1 - convert= 1./exchangeArea; -} - -/* destructor (shoudn't be modified) */ -LavaHCLFluxModel::~LavaHCLFluxModel() { - if ( properties != 0 ) delete properties; -} - -/* accessor to the name of the model */ -string LavaHCLFluxModel::getName(){ - return name; -} - -/* ****************** */ -/* Model for the flux */ -/* ****************** */ - -double LavaHCLFluxModel::getValue(double* valueOf - , const double& bt, const double& et, const double& at){ - - if ( bt - arrivalTime < 0 ) return 0.; - - - /* getting the hours since eruption */ - double hoursSinceArrival = (bt-arrivalTime)/3600.; - if ( hoursSinceArrival > refHours.back() ) return 0.; - - /* getting the index in the vector of fluxes */ - size_t hind = 0; - size_t nhours = refHours.size(); - while ( hind+1 < nhours and refHours[hind+1] < hoursSinceArrival ) hind++; - /* interpolation of the flux between the values */ - double beta = (hoursSinceArrival-refHours[hind]) - /(refHours[hind+1]-refHours[hind]); - double flux = beta*refFlows[hind+1] + (1.-beta)*refFlows[hind]; - double HCL = flux * 0.00033 *0.5; -// cout << " fluxhcl" << flux << endl; -// cout << "LavaLaze " << params->getDouble("LavaHCLFlux.activeArea") +1 << endl ; - if((bt-at) < (35*3600)) return HCL /((params->getDouble("LavaHCLFlux.activeArea"))+1.); - return 0; -// return convert*flux/vaporFlux.activeArea; - -} - -} /* namespace libforefire */ diff --git a/src/flux/LavaHeatFluxModel.cpp b/src/flux/LavaHeatFluxModel.cpp deleted file mode 100644 index 95a14b15..00000000 --- a/src/flux/LavaHeatFluxModel.cpp +++ /dev/null @@ -1,169 +0,0 @@ -/** - * @file LavaHeatFluxModel.cpp - * @brief TODO: add a brief description. - * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. - * @license This program is free software; See LICENSE file for details. (See LICENSE file). - * @author Jean‑Baptiste Filippi — 2025 - */ - -#include "../FluxModel.h" -#include "../FireDomain.h" -#include "../include/Futils.h" -using namespace std; - -namespace libforefire { - -class LavaHeatFluxModel: public FluxModel { - - /*! name the model */ - static const string name; - - /*! boolean for initialization */ - static int isInitialized; - - /*! properties needed by the model */ - size_t windU; - size_t windV; - - /*! coefficients needed by the model */ - double eruptionTime; - double heatflux; - double crustTemperature; - double lavaTemperature; - vector refHours; - vector crustFractions; - vector windValues; - vector A; - vector B; - - /*! local variables */ - - /*! result of the model */ - double getValue(double*, const double& - , const double&, const double&); - -public: - - LavaHeatFluxModel(const int& = 0, DataBroker* = 0); - virtual ~LavaHeatFluxModel(); - - string getName(); -}; - -FluxModel* getLavaHeatFluxModel(const int& = 0, DataBroker* = 0); - -/* name of the model */ -const string LavaHeatFluxModel::name = "LavaHeatFluxModel"; - -/* instantiation */ -FluxModel* getLavaHeatFluxModel(const int& index, DataBroker* db) { - return new LavaHeatFluxModel(index, db); -} - -/* registration */ -int LavaHeatFluxModel::isInitialized = - FireDomain::registerFluxModelInstantiator(name, getLavaHeatFluxModel ); - -/* constructor */ -LavaHeatFluxModel::LavaHeatFluxModel( - const int & mindex, DataBroker* db) - : FluxModel(mindex, db) { - /* defining the properties needed for the model */ - windU = registerProperty("windU"); - windV = registerProperty("windV"); - /* allocating the vector for the values of these properties */ - if ( numProperties > 0 ) properties = new double[numProperties]; - /* registering the model in the data broker */ - dataBroker->registerFluxModel(this); - - /* Definition of the coefficients */ - eruptionTime = 0.; - if ( params->isValued("lava.eruptionTime") ) - eruptionTime = params->getDouble("lava.eruptionTime"); - if ( !params->isValued("lava.hours") ) - cout<<"ERROR: vector of parameters lava.hours should be valued"<getDoubleArray("lava.hours"); - if ( !params->isValued("lava.crustFractions") ) - cout<<"ERROR: vector of parameters lava.crustFractions should be valued"<getDoubleArray("lava.crustFractions"); - if ( !params->isValued("lava.windTresholds") ) - cout<<"ERROR: vector of parameters lava.windTresholds should be valued"<getDoubleArray("lava.windTresholds"); - if ( !params->isValued("lava.A") ) - cout<<"ERROR: vector of parameters lava.A should be valued"<getDoubleArray("lava.A"); - if ( !params->isValued("lava.B") ) - cout<<"ERROR: vector of parameters lava.B should be valued"<getDoubleArray("lava.B"); - crustTemperature = 400.; - if ( params->isValued("lava.crustTemperature") ) - crustTemperature = params->getDouble("lava.crustTemperature"); - lavaTemperature = 800.; - if ( params->isValued("lava.temperature") ) - lavaTemperature = params->getDouble("lava.temperature"); -} - -/* destructor (shoudn't be modified) */ -LavaHeatFluxModel::~LavaHeatFluxModel() { - if ( properties != 0 ) delete properties; -} - -/* accessor to the name of the model */ -string LavaHeatFluxModel::getName(){ - return name; -} - -/* ****************** */ -/* Model for the flux */ -/* ****************** */ - -double LavaHeatFluxModel::getValue(double* valueOf - , const double& bt, const double& et, const double& at){ - - if ( bt - eruptionTime < 0 ) return 0.; -// if(params->isValued(getName()+".activeArea")) -// cout<getDouble(getName()+".activeArea")< refHours.back() ) return 0.; - - /* getting the fraction of crust */ - size_t hind = 0; - size_t nhours = refHours.size(); - while ( hind+1 < nhours and refHours[hind+1] < hoursSinceEruption ) hind++; - double beta = (hoursSinceEruption-refHours[hind]) - /(refHours[hind+1]-refHours[hind]); - double crustFraction = beta*crustFractions[hind+1] - + (1.-beta)*crustFractions[hind]; - - /* getting the mean temperature */ - double mean_temp = crustFraction*crustTemperature - + (1.-crustFraction)*lavaTemperature; - - /* getting the wind module */ - double windModule = sqrt(valueOf[windU]*valueOf[windU]+valueOf[windV]*valueOf[windV]); - - /* if the wind module is larger than 10 */ - if ( windModule > 10. ) return A[4]*mean_temp - B[4]; - /* else interpolating between two known values */ - /* getting the index for the coefficients */ - size_t wind = 0; - size_t nwind = windValues.size(); - while ( wind+1 < nwind and windValues[wind+1] < windModule ) wind++; - - /* computing the values for the interval boundaries */ - double leftval = A[wind]*mean_temp - B[wind]; - double rightval = A[wind+1]*mean_temp - B[wind+1]; - - /* linear interpolation between the boundaries */ - beta = (windModule-windValues[wind])/(windValues[wind+1]-windValues[wind]); -// return beta*rightval + (1.-beta)*leftval; - double coef = 1; // TODO fluxes are divided by 4 arbitrarily - heatflux=coef*(beta*rightval + (1.-beta)*leftval); -// cout << "heatflux" << heatflux << endl; -// return coef*(beta*rightval + (1.-beta)*leftval); - return heatflux; -} - -} /* namespace libforefire */ diff --git a/src/flux/LavaLazeFluxModel.cpp b/src/flux/LavaLazeFluxModel.cpp deleted file mode 100644 index 2fcc0817..00000000 --- a/src/flux/LavaLazeFluxModel.cpp +++ /dev/null @@ -1,141 +0,0 @@ -/** - * @file LavaLazeFluxModel.cpp - * @brief TODO: add a brief description. - * @copyright Copyright (C) 2025 ForeFire, Fire Team, SPE, CNRS/Universita di Corsica. - * @license This program is free software; See LICENSE file for details. (See LICENSE file). - * @author Jean‑Baptiste Filippi — 2025 - */ - -#include "../FluxModel.h" -#include "../FireDomain.h" -#include "../include/Futils.h" - -using namespace std; -namespace libforefire { - -class LavaLazeFluxModel: public FluxModel { - - /*! name the model */ - static const string name; - - /*! boolean for initialization */ - static int isInitialized; - - /*! properties needed by the model */ - - /*! coefficients needed by the model */ - double arrivalTime; - vector refHours; - vector refFlows; - double exchangeArea; - - /*! local variables */ - double convert; - - /*! result of the model */ - double getValue(double*, const double& - , const double&, const double&); - -public: - - LavaLazeFluxModel(const int& = 0, DataBroker* = 0); - virtual ~LavaLazeFluxModel(); - - string getName(); -}; - -FluxModel* getLavaLazeFluxModel(const int& = 0, DataBroker* = 0); - -/* name of the model */ -const string LavaLazeFluxModel::name = "LavaLazeFlux"; - -/* instantiation */ -FluxModel* getLavaLazeFluxModel(const int& index, DataBroker* db) { - return new LavaLazeFluxModel(index, db); -} - -/* registration */ -int LavaLazeFluxModel::isInitialized = - FireDomain::registerFluxModelInstantiator(name, getLavaLazeFluxModel ); - -/* constructor */ -LavaLazeFluxModel::LavaLazeFluxModel( - const int & mindex, DataBroker* db) - : FluxModel(mindex, db) { - /* defining the properties needed for the model */ - /* allocating the vector for the values of these properties */ - if ( numProperties > 0 ) properties = new double[numProperties]; - /* registering the model in the data broker */ - dataBroker->registerFluxModel(this); - - /* Definition of the coefficients */ - arrivalTime = 5600.; - if(params->isValued("vaporFlux.activeArea")) -// cout<getDouble("vaporFlux.activeArea")<isValued("laze.arrivalTime") ) - arrivalTime = params->getDouble("laze.arrivalTime"); - if ( !params->isValued("laze.hours") ) - cout<<"ERROR: vector of parameters laze.hours should be valued"<getDoubleArray("laze.hours"); - if ( !params->isValued("laze.flows") ) - cout<<"ERROR: vector of parameters laze.flows should be valued"<getDoubleArray("laze.flows"); - exchangeArea = 80000.; - if ( params->isValued("laze.exchangeArea") ) - exchangeArea = params->getDouble("laze.exchangeArea"); - /* coefficients */ - // Dividing by the final area of exchange to convert to kg.m-2.s-1 - convert= 1./exchangeArea; -} - -/* destructor (shoudn't be modified) */ -LavaLazeFluxModel::~LavaLazeFluxModel() { - if ( properties != 0 ) delete properties; -} - -/* accessor to the name of the model */ -string LavaLazeFluxModel::getName(){ - return name; -} - -/* ****************** */ -/* Model for the flux */ -/* ****************** */ - -double LavaLazeFluxModel::getValue(double* valueOf - , const double& bt, const double& et, const double& at){ - -// if ( bt - arrivalTime < 0 ) return 0.; - - - /* getting the hours since eruption */ - double hoursSinceArrival = (bt-arrivalTime)/3600.; - if ( hoursSinceArrival > refHours.back() ) return 0.; - - /* getting the index in the vector of fluxes */ - size_t hind = 0; - size_t nhours = refHours.size(); - while ( hind+1 < nhours and refHours[hind+1] < hoursSinceArrival ) hind++; - /* interpolation of the flux between the values */ - double beta = (hoursSinceArrival-refHours[hind]) - /(refHours[hind+1]-refHours[hind]); - double flux = beta*refFlows[hind+1] + (1.-beta)*refFlows[hind]; - -// cout << "LavaLaze before " << params->getDouble("LavaLazeFlux.activeArea") << endl ; - //if(params->getDouble("LavaLazeFlux.activeArea") < 1 ) return 1; -// double active= params->getDouble("LavaLazeFlux.activeArea") ; -// if (active < 1 ) cout << "LavaLaze " << active << endl ; -// cout << "LavaLaze " << active << endl ; -// cout << " fluxh2o " << flux << endl; -// cout << "LavaLaze " << params->getDouble("LavaLazeFlux.activeArea") +1 << endl ; - if((bt-at) < (35*3600)) return flux/(params->getDouble("LavaLazeFlux.activeArea") +1.); - -//params->getDouble("LavaLazeFlux.activeArea") - - return 0; -// return convert*flux/vaporFlux.activeArea; -// return flux/params->getDouble("LavaLazeFlux.activeArea"); - -} - -} /* namespace libforefire */ diff --git a/src/flux/SpottingFluxBasicModel.cpp b/src/flux/SpottingFluxBasicModel.cpp index 3ec14816..f29be52c 100644 --- a/src/flux/SpottingFluxBasicModel.cpp +++ b/src/flux/SpottingFluxBasicModel.cpp @@ -75,9 +75,9 @@ SpottingFluxBasicModel::SpottingFluxBasicModel( const int & mindex, DataBroker* db) : FluxModel(mindex, db) { /* defining the properties needed for the model */ - spot0 = registerProperty("fuel.Spot"); + //spot0 = registerProperty("fuel.Spot"); sigmad = registerProperty("fuel.Sigmad"); - + spot0 = sigmad; /* allocating the vector for the values of these properties */ if ( numProperties > 0 ) properties = new double[numProperties]; diff --git a/src/include/Version.h b/src/include/Version.h index 759b7ed7..cdbe3b49 100644 --- a/src/include/Version.h +++ b/src/include/Version.h @@ -1,3 +1,3 @@ #pragma once -const char* ff_version = "v2.2.1"; +const char* ff_version = "v2.3.0"; diff --git a/tests/run.bash b/tests/run.bash index af839119..1647e59b 100755 --- a/tests/run.bash +++ b/tests/run.bash @@ -41,7 +41,7 @@ fi # Run additional tests (e.g., runff and runANN) run_test "runff" "runff" -run_test "runANN" "runff" # adjust the directory if runANN is in a different location +run_test "runANN" "runANN" # Final summary echo "--------------------------" @@ -61,4 +61,3 @@ else echo "All tests passed." exit 0 fi -./clean.bash diff --git a/tools/devops/Dockerfile.base b/tools/devops/Dockerfile.base deleted file mode 100644 index 6ca310c0..00000000 --- a/tools/devops/Dockerfile.base +++ /dev/null @@ -1,22 +0,0 @@ -FROM ubuntu:22.04 - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - build-essential \ - libnetcdf-c++4-dev \ - cmake - -WORKDIR /forefire -ENV FOREFIREHOME=/forefire - -# we could only copy src, cmakelists.txt and cmake-build.sh -COPY . . - -# install forefire -RUN sh cmake-build.sh - -# add executable to PATH -RUN cp /forefire/bin/forefire /usr/local/bin/ - -# Set the entrypoint to bash for interactive sessions -CMD ["bash"] \ No newline at end of file diff --git a/tools/devops/Dockerfile.multistage b/tools/devops/Dockerfile.multistage deleted file mode 100644 index 8bfcc217..00000000 --- a/tools/devops/Dockerfile.multistage +++ /dev/null @@ -1,60 +0,0 @@ -# File: tools/Dockerfile.multistage -# Target: Creates an optimized, smaller final image using a multi-stage build. -# Contains full Python support. - -# --- STAGE 1: The "Builder" --- -# This stage is a temporary environment used to compile the C++ code. -# It contains all the heavy build tools, which will NOT be in the final image. -FROM ubuntu:22.04 AS builder - -# Install build tools and C++ dependencies -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - build-essential \ - libnetcdf-c++4-dev \ - cmake \ - git && \ - rm -rf /var/lib/apt/lists/* - -WORKDIR /src - -# Copy all source code into the builder stage -COPY . . - -# Build the C++ library and executable -RUN sh cmake-build.sh - - -# --- STAGE 2: The "Final Image" --- -# This is the final, clean image that will be distributed. -# It starts from a fresh Ubuntu base and only pulls in what's necessary. -FROM ubuntu:22.04 - -# Install only the RUNTIME dependencies needed for ForeFire and the Python bindings. -# As requested, we use libnetcdf-c++4-dev here for consistency with the build stage. -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - libnetcdf-c++4-dev \ - libgomp1 \ - python3 \ - g++ \ - python3-pip \ - python3-dev && \ - rm -rf /var/lib/apt/lists/* - -WORKDIR /forefire -ENV FOREFIREHOME=/forefire - -# The magic step: Copy the ENTIRE built project from the 'builder' stage. -# This brings over the compiled libs, binaries, and the source code needed for the bindings. -COPY --from=builder /src/ . - -# Add the forefire executable to the system's PATH -RUN cp /forefire/bin/forefire /usr/local/bin/ - -# Install the Python bindings and their dependencies (numpy, etc.) -# This compiles the C++ extension using the headers we just copied. -RUN pip3 install ./bindings/python - -# Set the default command to start a bash shell -CMD ["bash"] \ No newline at end of file diff --git a/tools/devops/entrypoint.sh b/tools/devops/entrypoint.sh deleted file mode 100644 index fa15f6e4..00000000 --- a/tools/devops/entrypoint.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/bash -set -e - -echo "šŸ”„ ForeFire Quick Start Demo" -echo "==============================" -echo "" - -# Navigate to the test directory -cd /forefire/tests/runff - -echo "šŸ“ Working directory: $(pwd)" -echo "🌐 Starting ForeFire with HTTP server..." -echo "" -echo "Once started, open your browser to: http://localhost:8000" -echo "To run the demo simulation, type: include[real_case.ff]" -echo "To stop, press Ctrl+C" -echo "" - -# Function to handle graceful shutdown -cleanup() { - echo "" - echo "šŸ›‘ Shutting down ForeFire..." - exit 0 -} - -# Set up signal handlers for graceful shutdown -trap cleanup SIGINT SIGTERM - -# Start forefire and automatically launch HTTP server -forefire -l & -FOREFIRE_PID=$! - -# Wait for the forefire process -wait $FOREFIRE_PID diff --git a/tools/devops/increment-version.yml b/tools/devops/increment-version.yml index 88ae1051..b3ce591e 100644 --- a/tools/devops/increment-version.yml +++ b/tools/devops/increment-version.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Git configuration run: | diff --git a/tools/devops/readme.md b/tools/devops/readme.md deleted file mode 100644 index 8726ea1d..00000000 --- a/tools/devops/readme.md +++ /dev/null @@ -1,10 +0,0 @@ -## Alternative Dockerfiles - -WIP - -- Dockerfile without python to make the image smaller -- Multistage build to make the also smaller (490 vs 620 mb) - -``` -docker build -f tools/docker/Dockerfile.multistage -t forefire:multistage . -``` \ No newline at end of file diff --git a/tools/htdocs/index.html b/tools/htdocs/index.html index 0b16b192..5fd81642 100644 --- a/tools/htdocs/index.html +++ b/tools/htdocs/index.html @@ -29,6 +29,37 @@
    +
    +
    Layer Colorbar
    + +
    + N/A + N/A +
    +
    + + + + +
    +
    + + +
    +
    +
    @@ -54,7 +85,7 @@ @@ -66,4 +97,4 @@ - \ No newline at end of file + diff --git a/tools/htdocs/js/forefireGUI.js b/tools/htdocs/js/forefireGUI.js index c336ab2e..3f4effe0 100644 --- a/tools/htdocs/js/forefireGUI.js +++ b/tools/htdocs/js/forefireGUI.js @@ -24,6 +24,20 @@ const commands = { // Populate predefined command buttons. const commandButtonsDiv = document.getElementById('commandButtons'); + const responseConsoleEl = document.getElementById('responseConsole'); + const commandHistoryEl = document.getElementById('commandHistory'); + + function uiLog(message, type = 'info') { + console.log(`[UI ${type}] ${message}`); + if (responseConsoleEl) { + const div = document.createElement('div'); + div.className = `ui-log ui-${type}`; + div.textContent = message; + responseConsoleEl.appendChild(div); + responseConsoleEl.scrollTop = responseConsoleEl.scrollHeight; + } + } + for (const cmd in commands) { const btn = document.createElement('button'); btn.textContent = cmd; @@ -35,7 +49,7 @@ const commands = { } // Initialize Leaflet map. - const map = L.map('map').setView([42.0, 9.0], 8); + const map = L.map('map', { doubleClickZoom: false }).setView([42.0, 9.0], 8); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: 'Ā© OpenStreetMap contributors' }).addTo(map); @@ -43,6 +57,315 @@ const commands = { let mapLayer = null; let imageOverlay = null; // For image overlays let velocityLayer = null; // For wind velocity layer + let activeLayer = null; // Track the last clicked layer + let lastPlotCommand = null; + // --- Colorbar UI State --- + const colorbarElements = { + canvas: document.getElementById('colorbarCanvas'), + minLabel: document.getElementById('colorbarMinLabel'), + maxLabel: document.getElementById('colorbarMaxLabel'), + minInput: document.getElementById('colorbarMinInput'), + maxInput: document.getElementById('colorbarMaxInput'), + cmapSelect: document.getElementById('colorbarCmapSelect'), + autoFitCheckbox: document.getElementById('colorbarAutoFit'), + applyBtn: document.getElementById('colorbarApply'), + useDataBtn: document.getElementById('colorbarUseData'), + status: document.getElementById('colorbarStatus') + }; + colorbarElements.ctx = colorbarElements.canvas ? colorbarElements.canvas.getContext('2d') : null; + + const COLORBAR_GRADIENTS = { + viridis: ['#440154', '#46337f', '#365c8d', '#277f8e', '#1fa187', '#95d840', '#fde725'], + plasma: ['#0d0887', '#7e03a8', '#cc4778', '#f89540', '#f0f921'], + plasma_r: ['#f0f921', '#f89540', '#cc4778', '#7e03a8', '#0d0887'], + hot: ['#000000', '#ff0000', '#ffff00', '#ffffff'], + hot_r: ['#ffffff', '#ffff00', '#ff0000', '#000000'], + coolwarm: ['#3b4cc0', '#ffffff', '#b40426'], + grey: ['#000000', '#7f7f7f', '#ffffff'], + spring: ['#ff00ff', '#ffff00'], + jet: ['#00007f', '#007fff', '#00ff00', '#ff7f00', '#7f0000'], + turbo: ['#30123b', '#4145ab', '#2c87f0', '#29d7b1', '#8cf473', '#f9ec4f', '#f9a13c', '#d9381e'], + fuel: ['#120400', '#5f2c09', '#c76a17', '#f5c453', '#fff6cf'] + }; + const AVAILABLE_COLORMAPS = ['viridis', 'plasma', 'plasma_r', 'hot', 'hot_r', 'coolwarm', 'grey', 'spring', 'jet', 'turbo', 'fuel']; + const DEFAULT_COLORMAP = 'viridis'; + const INVALID_DATA_SENTINELS = new Set([-999, -9999]); + + const colorbarState = { + min: null, + max: null, + cmap: DEFAULT_COLORMAP, + autoRange: true, + lastDataMin: null, + lastDataMax: null, + userCmapOverride: false + }; + let suppressCmapEvent = false; + + function formatLabelValue(value) { + if (!Number.isFinite(value)) { + return 'N/A'; + } + const absVal = Math.abs(value); + if ((absVal > 0 && absVal < 0.001) || absVal >= 1000) { + return value.toExponential(2); + } + return value.toFixed(3); + } + + function drawColorbar(cmapName) { + if (!colorbarElements.ctx || !colorbarElements.canvas) return; + const palette = COLORBAR_GRADIENTS[cmapName] || COLORBAR_GRADIENTS[DEFAULT_COLORMAP]; + const ctx = colorbarElements.ctx; + const width = colorbarElements.canvas.width; + const height = colorbarElements.canvas.height; + const gradient = ctx.createLinearGradient(0, 0, width, 0); + const stops = palette.length - 1; + palette.forEach((color, index) => { + const stop = stops === 0 ? 0 : index / stops; + gradient.addColorStop(stop, color); + }); + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, width, height); + } + + function updateColorbarStatus(message = '') { + if (colorbarElements.status) { + colorbarElements.status.textContent = message; + } + } + + function sanitizeDataValue(value) { + if (typeof value !== 'number' || !Number.isFinite(value)) { + return null; + } + if (INVALID_DATA_SENTINELS.has(value)) { + return null; + } + return value; + } + + function updateColorbarData(minVal, maxVal) { + const dataMin = sanitizeDataValue(minVal); + const dataMax = sanitizeDataValue(maxVal); + + colorbarState.lastDataMin = dataMin; + colorbarState.lastDataMax = dataMax; + + if (colorbarElements.minLabel) { + colorbarElements.minLabel.textContent = formatLabelValue(dataMin ?? NaN); + } + if (colorbarElements.maxLabel) { + colorbarElements.maxLabel.textContent = formatLabelValue(dataMax ?? NaN); + } + + if (colorbarState.autoRange) { + if (dataMin !== null && dataMax !== null && dataMin < dataMax) { + colorbarState.min = dataMin; + colorbarState.max = dataMax; + if (colorbarElements.minInput) { + colorbarElements.minInput.value = dataMin; + } + if (colorbarElements.maxInput) { + colorbarElements.maxInput.value = dataMax; + } + updateColorbarStatus('Using data range'); + } else { + colorbarState.min = null; + colorbarState.max = null; + if (colorbarElements.minInput) { + colorbarElements.minInput.value = ''; + } + if (colorbarElements.maxInput) { + colorbarElements.maxInput.value = ''; + } + updateColorbarStatus('Data range unavailable'); + } + } else { + updateColorbarStatus('Custom range locked'); + } + } + + function setColorbarCmap(newCmap, userInitiated = false) { + const cmap = AVAILABLE_COLORMAPS.includes(newCmap) ? newCmap : DEFAULT_COLORMAP; + colorbarState.cmap = cmap; + colorbarState.userCmapOverride = userInitiated; + if (colorbarElements.cmapSelect) { + suppressCmapEvent = true; + colorbarElements.cmapSelect.value = cmap; + suppressCmapEvent = false; + } + drawColorbar(cmap); + } + + function handleColorbarApply() { + if (!colorbarElements.minInput || !colorbarElements.maxInput) return; + const minValue = colorbarElements.minInput.value.trim(); + const maxValue = colorbarElements.maxInput.value.trim(); + if (minValue === '' || maxValue === '') { + updateColorbarStatus('Provide both min and max values.'); + return; + } + const parsedMin = Number(minValue); + const parsedMax = Number(maxValue); + if (!Number.isFinite(parsedMin) || !Number.isFinite(parsedMax)) { + updateColorbarStatus('Values must be numeric.'); + return; + } + if (parsedMin >= parsedMax) { + updateColorbarStatus('Min must be lower than max.'); + return; + } + colorbarState.min = parsedMin; + colorbarState.max = parsedMax; + colorbarState.autoRange = false; + syncAutoFitCheckbox(); + updateColorbarStatus('Custom range applied.'); + replotActiveLayer("colorbar apply"); + } + + function handleColorbarUseData() { + colorbarState.autoRange = true; + syncAutoFitCheckbox(); + if (colorbarState.lastDataMin !== null && colorbarState.lastDataMax !== null && colorbarState.lastDataMin < colorbarState.lastDataMax) { + colorbarState.min = colorbarState.lastDataMin; + colorbarState.max = colorbarState.lastDataMax; + if (colorbarElements.minInput) { + colorbarElements.minInput.value = colorbarState.lastDataMin; + } + if (colorbarElements.maxInput) { + colorbarElements.maxInput.value = colorbarState.lastDataMax; + } + updateColorbarStatus('Using data range'); + } else { + colorbarState.min = null; + colorbarState.max = null; + if (colorbarElements.minInput) { + colorbarElements.minInput.value = ''; + } + if (colorbarElements.maxInput) { + colorbarElements.maxInput.value = ''; + } + updateColorbarStatus('Waiting for data range'); + } + replotActiveLayer("data range"); + } + + function replotActiveLayer(reason = "colorbar update") { + if (!activeLayer) { + uiLog("Select a layer before applying colorbar changes", "warn"); + return; + } + runLayerPlot(activeLayer, false, reason); + } + + function syncAutoFitCheckbox() { + if (colorbarElements.autoFitCheckbox) { + colorbarElements.autoFitCheckbox.checked = !!colorbarState.autoRange; + } + } + + function ensureColorbarDefaultsForLayer(layerName) { + if (layerName === 'fuel' && !colorbarState.userCmapOverride && colorbarState.cmap !== 'fuel') { + setColorbarCmap('fuel', false); + } + } + + function getRangeParam(layerName) { + if (Number.isFinite(colorbarState.min) && Number.isFinite(colorbarState.max) && colorbarState.min < colorbarState.max) { + return `;range=(${colorbarState.min},${colorbarState.max})`; + } + if (layerName === 'fuel') { + return ';range=(0,255)'; + } + return ''; + } + + function getCmapParam(layerName) { + let cmap = colorbarState.cmap; + if (!cmap) { + cmap = layerName === 'fuel' ? 'fuel' : DEFAULT_COLORMAP; + } + return cmap ? `;cmap=${cmap}` : ''; + } + + function handlePlotMetadata(layerName, metadata) { + if (!metadata || typeof metadata !== 'object') { + return; + } + if (layerName === 'wind' || layerName === 'windU' || layerName === 'windV') { + return; + } + if (Object.prototype.hasOwnProperty.call(metadata, 'minVal') || Object.prototype.hasOwnProperty.call(metadata, 'maxVal')) { + updateColorbarData(metadata.minVal, metadata.maxVal); + } + } + + function buildPlotCommand(layerName) { + ensureColorbarDefaultsForLayer(layerName); + const bboxSegment = getBBoxParam(); + const rangeSegment = getRangeParam(layerName); + const cmapSegment = getCmapParam(layerName); + const filename = `${layerName}.png`; + return `plot[parameter=${layerName};filename=${filename};size=320,320${rangeSegment}${cmapSegment};projectionOut=json${bboxSegment}]`; + } + + function extractLayerFromPlotCommand(commandString) { + if (typeof commandString !== 'string') { + return null; + } + const match = commandString.match(/plot\[(.*?)\]/i); + if (!match) { + return null; + } + const inner = match[1]; + const paramMatch = inner.match(/parameter=([^;]+)/i); + if (!paramMatch) { + return null; + } + return paramMatch[1]; + } + + function initializeColorbarUI() { + if (colorbarElements.cmapSelect) { + AVAILABLE_COLORMAPS.forEach((name) => { + const option = document.createElement('option'); + option.value = name; + option.textContent = name; + colorbarElements.cmapSelect.appendChild(option); + }); + colorbarElements.cmapSelect.addEventListener('change', (event) => { + if (suppressCmapEvent) { + return; + } + setColorbarCmap(event.target.value, true); + replotActiveLayer("colormap change"); + }); + } + if (colorbarElements.autoFitCheckbox) { + colorbarElements.autoFitCheckbox.checked = colorbarState.autoRange; + colorbarElements.autoFitCheckbox.addEventListener('change', (event) => { + colorbarState.autoRange = event.target.checked; + if (colorbarState.autoRange) { + handleColorbarUseData(); + } else { + updateColorbarStatus('Custom range locked'); + replotActiveLayer("autofit off"); + } + }); + } + if (colorbarElements.applyBtn) { + colorbarElements.applyBtn.addEventListener('click', handleColorbarApply); + } + if (colorbarElements.useDataBtn) { + colorbarElements.useDataBtn.addEventListener('click', handleColorbarUseData); + } + setColorbarCmap(DEFAULT_COLORMAP, false); + updateColorbarStatus('Waiting for data range'); + } + + initializeColorbarUI(); // --- Bounding Box Code --- let bboxRect = null; @@ -152,33 +475,33 @@ const commands = { map.fitBounds(mapLayer.getBounds()); } } -// Update map with GeoJSON overlay. -function updateMapWithGeoJSON(geojson, refit = true) { - if (mapLayer) { + // Update map with GeoJSON overlay. + function updateMapWithGeoJSON(geojson, refit = false) { + if (mapLayer) { map.removeLayer(mapLayer); - } + } - // Define fire-like colors for styling - const fireStyle = { + // Define fire-like colors for styling + const fireStyle = { color: "#ff4500", // Fire orange-red outline weight: 2, // Line thickness opacity: 1, // Full opacity for edges fillColor: "#ff6600", // Fire orange fill fillOpacity: 0.7 // Semi-transparent fill - }; + }; - // Apply the fire-style to the GeoJSON layer - mapLayer = L.geoJSON(geojson, { + // Apply the fire-style to the GeoJSON layer + mapLayer = L.geoJSON(geojson, { style: function (feature) { - return fireStyle; + return fireStyle; } - }).addTo(map); + }).addTo(map); - // Fit map to the bounds of the new GeoJSON layer if refit is true - if (refit && mapLayer.getBounds && mapLayer.getBounds().isValid()) { + // Fit map to the bounds of the new GeoJSON layer if refit is true + if (refit && mapLayer.getBounds && mapLayer.getBounds().isValid()) { map.fitBounds(mapLayer.getBounds()); + } } -} // Add image overlay. @@ -186,21 +509,24 @@ function updateMapWithGeoJSON(geojson, refit = true) { if (imageOverlay) { map.removeLayer(imageOverlay); } + // Bust cache so freshly generated image is fetched. + const cacheBustedUrl = imageUrl ? `${imageUrl}${imageUrl.includes('?') ? '&' : '?'}_cb=${Date.now()}` : imageUrl; const leafletBounds = [[bounds.SWlat, bounds.SWlon], [bounds.NElat, bounds.NElon]]; - imageOverlay = L.imageOverlay(imageUrl, leafletBounds, { opacity: 0.7 }); + imageOverlay = L.imageOverlay(cacheBustedUrl, leafletBounds, { opacity: 0.7 }); imageOverlay.addTo(map); imageOverlay.setZIndex(0); } // Send command function: auto-prefix with "ff:". async function sendCommand(command) { - const responseDiv = document.getElementById('responseConsole'); - const historyDiv = document.getElementById('commandHistory'); + const responseDiv = responseConsoleEl || document.getElementById('responseConsole'); + const historyDiv = commandHistoryEl || document.getElementById('commandHistory'); historyDiv.innerHTML += "
    " + command + "
    "; historyDiv.scrollTop = historyDiv.scrollHeight; responseDiv.innerHTML += "
    forefire >" + command + "
    "; responseDiv.scrollTop = responseDiv.scrollHeight; - + const originalCommand = command; + if (!command.startsWith("ff:")) { command = "ff:" + command; } @@ -230,6 +556,15 @@ function updateMapWithGeoJSON(geojson, refit = true) { responseDiv.scrollTop = responseDiv.scrollHeight; const trimmed = text.trim(); + if (trimmed.startsWith("{") && trimmed.endsWith("}")) { + try { + const metadata = JSON.parse(trimmed); + const derivedLayer = extractLayerFromPlotCommand(originalCommand); + handlePlotMetadata(derivedLayer, metadata); + } catch (e) { + // Ignore JSON parse issues for non-plot responses. + } + } if (trimmed.startsWith("Error: " + error + ""; return null; } finally { - updateISODate(); + updateISODate(); } } @@ -282,108 +617,128 @@ function updateMapWithGeoJSON(geojson, refit = true) { // --- Layers Sidebar and Update --- async function updateLayerList() { + uiLog("Requesting layer list..."); const layersResponse = await sendCommand("getParameter[layerNames]"); - if (layersResponse) { - const layers = layersResponse.trim().split(/\s*,\s*/); - const layerListEl = document.getElementById('layerList'); - layerListEl.innerHTML = ""; - let hasWindU = false, hasWindV = false; - layers.forEach(layer => { - const li = document.createElement('li'); - li.textContent = layer; - li.addEventListener('click', () => { - // Build the appropriate ff:plot command. - let cmd = ""; - if (layer === "fuel") { - cmd = `plot[parameter=fuel;filename=fuel.png;range=(0,255);cmap=fuel;projectionOut=json${getBBoxParam()}]`; - } else if (layer === "windU" || layer === "windV") { - cmd = `plot[parameter=${layer};filename=${layer}.png;size=320,320;projectionOut=json${getBBoxParam()}]`; - if (layer === "windU") hasWindU = true; - if (layer === "windV") hasWindV = true; - } else { - cmd = `plot[parameter=${layer};filename=${layer}.png;size=320,320;projectionOut=json${getBBoxParam()}]`; - } - sendCommand(cmd).then(responseText => { - try { - const bounds = JSON.parse(responseText); - // If no bounding box exists yet, use these bounds. - if (!bboxRect && bounds.SWlon !== undefined) { - createBoundingBoxFromResponse(bounds); - } - const imageUrl = cmd.match(/filename=([^;]+)/)[1]; - addImageOverlay(imageUrl, bounds); - } catch (err) { - console.error("Error parsing overlay bounds:", err); - } - }); - }); - layerListEl.appendChild(li); + if (!layersResponse) { + uiLog("Layer list request returned no data", "warn"); + return; + } + const layers = layersResponse.trim().split(/\s*,\s*/).filter(Boolean); + const layerListEl = document.getElementById('layerList'); + layerListEl.innerHTML = ""; + activeLayer = null; + layers.forEach(layer => { + const li = document.createElement('li'); + li.textContent = layer; + li.addEventListener('click', () => { + activeLayer = layer; + runLayerPlot(layer, true, "layer click"); }); - // If both windU and windV exist, add an extra "wind" button. - if (layers.includes("windU") && layers.includes("windV")) { - const li = document.createElement('li'); - li.textContent = "wind"; - li.style.fontWeight = "bold"; - li.addEventListener('click', () => { - // Toggle wind layer: if it exists, remove it. - if (velocityLayer) { - map.removeLayer(velocityLayer); - velocityLayer = null; - return; + layerListEl.appendChild(li); + }); + // If both windU and windV exist, add an extra "wind" button. + if (layers.includes("windU") && layers.includes("windV")) { + const li = document.createElement('li'); + li.textContent = "wind"; + li.style.fontWeight = "bold"; + li.addEventListener('click', () => { + // Toggle wind layer: if it exists, remove it. + if (velocityLayer) { + map.removeLayer(velocityLayer); + velocityLayer = null; + uiLog("Wind layer removed"); + return; + } + activeLayer = "wind"; + let cmd = `plot[parameter=wind;filename=windCS.json;size=60,60;projectionOut=json${getBBoxParam()}]`; + sendCommand(cmd).then(responseText => { + let bboxData; + try { + bboxData = JSON.parse(responseText); + } catch (e) { + console.error("Error parsing bounding box data:", e); + uiLog("Wind plot response was not JSON", "warn"); } - let cmd = `plot[parameter=wind;filename=windCS.json;size=60,60;projectionOut=json${getBBoxParam()}]`; - sendCommand(cmd).then(responseText => { - let bboxData; - try { - bboxData = JSON.parse(responseText); - } catch (e) { - console.error("Error parsing bounding box data:", e); - } - // If no bounding box exists yet and the response contains bounds, create one. - if (!bboxRect && bboxData && bboxData.SWlon !== undefined) { - createBoundingBoxFromResponse(bboxData); - } - // Use minVal and maxVal from bboxData if available. - let windMin = (bboxData && bboxData.minVal !== undefined) ? bboxData.minVal : 0; - let windMax = (bboxData && bboxData.maxVal !== undefined) ? bboxData.maxVal : 10; - console.log("Wind min/max:", windMin, windMax); - // Now fetch the actual wind data from the file windCS.json. - fetch("windCS.json") - .then(resp => resp.json()) - .then(windData => { - if (velocityLayer) { - map.removeLayer(velocityLayer); - } - velocityLayer = L.velocityLayer({ - displayValues: true, - displayOptions: { - velocityType: "Wind", - position: "bottomleft", - emptyString: "No velocity data", - angleConvention: "bearingCW", - showCardinal: false, - useVectors: true, - speedUnit: "ms", - directionString: "Direction", - speedString: "Speed" - }, - data: windData, - lineWidth: 3, - minVelocity: windMin, - maxVelocity: windMax, - velocityScale: 0.005, - opacity: 1, - paneName: "overlayPane" - }); - velocityLayer.addTo(map); - }) - .catch(e => { - console.error("Error fetching wind data:", e); + // If no bounding box exists yet and the response contains bounds, create one. + if (!bboxRect && bboxData && bboxData.SWlon !== undefined) { + createBoundingBoxFromResponse(bboxData); + } + // Use minVal and maxVal from bboxData if available. + let windMin = (bboxData && bboxData.minVal !== undefined) ? bboxData.minVal : 0; + let windMax = (bboxData && bboxData.maxVal !== undefined) ? bboxData.maxVal : 10; + uiLog(`Wind min/max: ${windMin}/${windMax}`); + // Now fetch the actual wind data from the file windCS.json. + fetch(`windCS.json?_cb=${Date.now()}`) + .then(resp => resp.json()) + .then(windData => { + if (velocityLayer) { + map.removeLayer(velocityLayer); + } + velocityLayer = L.velocityLayer({ + displayValues: true, + displayOptions: { + velocityType: "Wind", + position: "bottomleft", + emptyString: "No velocity data", + angleConvention: "bearingCW", + showCardinal: false, + useVectors: true, + speedUnit: "ms", + directionString: "Direction", + speedString: "Speed" + }, + data: windData, + lineWidth: 3, + minVelocity: windMin, + maxVelocity: windMax, + velocityScale: 0.005, + opacity: 1, + paneName: "overlayPane" }); - }); + velocityLayer.addTo(map); + uiLog("Wind layer updated"); + }) + .catch(e => { + console.error("Error fetching wind data:", e); + uiLog("Failed to fetch wind data", "error"); + }); }); - layerListEl.appendChild(li); + }); + layerListEl.appendChild(li); + } + uiLog(`Layer list refreshed (${layers.length} items)`); + } + + async function runLayerPlot(layer, refit = true, reason = "rerender") { + activeLayer = layer; + ensureColorbarDefaultsForLayer(layer); + const cmd = buildPlotCommand(layer); + lastPlotCommand = cmd; + uiLog(`Plotting ${layer} (${reason})...`); + try { + const responseText = await sendCommand(cmd); + if (!responseText) { + uiLog(`No data returned for ${layer}`, "warn"); + return; + } + try { + const bounds = JSON.parse(responseText); + // If no bounding box exists yet, use these bounds. + if (!bboxRect && bounds.SWlon !== undefined) { + createBoundingBoxFromResponse(bounds); + } + const match = cmd.match(/filename=([^;]+)/); + if (match) { + addImageOverlay(match[1], bounds); + } + handlePlotMetadata(layer, bounds); + uiLog(`Overlay updated for ${layer}`); + } catch (err) { + console.error("Error parsing overlay bounds:", err); + uiLog(`Plot response for ${layer} was not JSON`, "warn"); } + } catch (err) { + uiLog(`Failed to plot ${layer}: ${err.message}`, "error"); } } @@ -440,18 +795,23 @@ function updateMapWithGeoJSON(geojson, refit = true) { let autoRefreshInterval = null; // 1) Factor out the refresh logic into a separate function: -async function refreshMap(refit = true) { +async function refreshMap(refit = false) { + const shouldRefit = (typeof refit === "boolean") ? refit : false; + uiLog(`Refreshing map${shouldRefit ? " with refit" : ""}...`); // The same logic you had in the 'refreshMap' button’s click handler await sendCommand("setParameter[dumpMode=geojson]"); const geojsonText = await sendCommand("print[]"); if (geojsonText) { try { const geojson = JSON.parse(geojsonText); - updateMapWithGeoJSON(geojson, refit); - } catch (e) { - console.error("Failed to parse GeoJSON:", e); + updateMapWithGeoJSON(geojson, shouldRefit); + } catch (e) { + console.error("Failed to parse GeoJSON:", e); + uiLog("GeoJSON parse failed during refresh", "error"); + } + } else { + uiLog("No GeoJSON returned during refresh", "warn"); } - } // If velocityLayer is active, refresh the wind data too: if (velocityLayer) { @@ -486,13 +846,19 @@ async function refreshMap(refit = true) { }) .catch(e => { console.error("Error fetching wind data:", e); + uiLog("Failed to refresh wind layer", "error"); }); } + // Refresh current image layer (if any) when live-refreshing. + if (activeLayer && activeLayer !== "wind") { + runLayerPlot(activeLayer, false, "auto-refresh"); + } + uiLog("Refresh complete"); } -// 2) Link existing "Refresh Map" button to our new function: -document.getElementById('refreshMap').addEventListener('click', refreshMap); +// 2) Link existing "Refresh Map" button to our new function without recentering: +document.getElementById('refreshMap').addEventListener('click', () => refreshMap(false)); // 3) Handle the auto-refresh checkbox changes: document.getElementById('autoRefreshCheckbox').addEventListener('change', (e) => { @@ -513,13 +879,15 @@ document.getElementById('WindOrParts').addEventListener('change', (e) => { }); const toggleLogs = document.getElementById('toggleLogs'); -const responseConsole = document.getElementById('responseConsole'); -const commandHistory = document.getElementById('commandHistory'); toggleLogs.addEventListener('change', function() { const displayStyle = this.checked ? 'block' : 'none'; - responseConsole.style.display = displayStyle; - commandHistory.style.display = displayStyle; + if (responseConsoleEl) { + responseConsoleEl.style.display = displayStyle; + } + if (commandHistoryEl) { + commandHistoryEl.style.display = displayStyle; + } // Optional: Adjust map height if needed when logs are hidden // For example, increase the map height when logs are hidden @@ -528,4 +896,4 @@ toggleLogs.addEventListener('change', function() { } else { document.getElementById('map').style.height = '500px'; } -}); \ No newline at end of file +}); diff --git a/tools/htdocs/style.css b/tools/htdocs/style.css index 65f4a53a..204481e5 100644 --- a/tools/htdocs/style.css +++ b/tools/htdocs/style.css @@ -19,7 +19,8 @@ html, body { padding: 10px; display: flex; flex-direction: column; - justify-content: space-between; + justify-content: flex-start; + gap: 10px; } /* Logo container */ #logo { @@ -58,6 +59,73 @@ html, body { cursor: pointer; margin-top: 10px; } +#colorbarPanel { + background-color: #1b1b1b; + border: 1px solid #333; + padding: 8px; + font-size: 0.85em; + display: flex; + flex-direction: column; + gap: 6px; +} +.colorbar-header { + text-transform: uppercase; + font-size: 0.75em; + letter-spacing: 0.08em; + color: #f0a500; +} +.colorbar-scale { + display: flex; + justify-content: space-between; + font-size: 0.75em; + color: #ccc; +} +.colorbar-controls { + display: flex; + flex-direction: column; + gap: 4px; +} +.colorbar-controls label { + display: flex; + flex-direction: column; + color: #bbb; + font-size: 0.75em; + gap: 2px; +} +.colorbar-controls .autofit-row { + flex-direction: row; + align-items: center; + gap: 6px; +} +.colorbar-controls input, +.colorbar-controls select { + background-color: #2c2c2c; + color: #eee; + border: 1px solid #444; + padding: 2px 4px; + font-size: 0.85em; +} +.colorbar-actions { + display: flex; + gap: 6px; +} +.colorbar-actions button { + background-color: #333; + color: #eee; + border: 1px solid #444; + padding: 4px 6px; + font-size: 0.75em; + cursor: pointer; + flex: 1; +} +.colorbar-actions button:hover { + background-color: #444; +} +#colorbarStatus { + min-height: 14px; + font-size: 0.7em; + color: #999; +} /* Right side: main area */ #mainArea { flex-grow: 1; @@ -85,6 +153,16 @@ html, body { height: 200px; overflow-y: auto; } +.ui-log { + color: #9ac7ff; + font-style: italic; +} +.ui-log.ui-warn { + color: #f0b429; +} +.ui-log.ui-error { + color: #ff7f7f; +} /* Command History: sent commands in green */ #commandHistory { background: #222; @@ -170,4 +248,4 @@ html, body { #sendCommand:hover { background-color: orange !important; /* A slightly darker orange for hover */ -} \ No newline at end of file +}