[
  {
    "path": ".clang-format",
    "content": "#see https://clang.llvm.org/docs/ClangFormatStyleOptions.html\nLanguage: Cpp\nColumnLimit: 100\nContinuationIndentWidth: 2\nUseTab: Never\nIndentWidth: 2\nTabWidth: 2\nIndentCaseLabels: true\nIncludeBlocks: Preserve\nSortIncludes: true\nSortUsingDeclarations: false\nAlignConsecutiveMacros: false\nAlignEscapedNewlines: DontAlign\nAlignAfterOpenBracket: AlwaysBreak\nAlignOperands: false\nAlignTrailingComments: false\nBinPackArguments: false\nBinPackParameters: false\nSpacesInContainerLiterals: false\nCpp11BracedListStyle: true\nAllowShortFunctionsOnASingleLine: Empty\nAllowShortIfStatementsOnASingleLine: Always\nFixNamespaceComments: false\nReflowComments: false\nNamespaceIndentation: All\nBreakStringLiterals: false\nConstructorInitializerIndentWidth: 2\nSpaceBeforeCtorInitializerColon: true\nBreakBeforeInheritanceComma: false\nBreakInheritanceList: AfterColon\nBreakConstructorInitializersBeforeComma: false\nBreakConstructorInitializers: AfterColon\nConstructorInitializerAllOnOneLineOrOnePerLine: true\n"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build\n\non:\n  workflow_dispatch\n  # push:\n  #   branches:\n  #     - main\n  # pull_request:\n  #   branches:\n  #     - main\n\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\nconcurrency:\n  group: \"pages\"\n  cancel-in-progress: true\n\njobs:\n  build-qgis-js:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-24.04\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set Swap Space\n        if: ${{ !env.ACT }}\n        uses: pierotofy/set-swap-space@master\n        with:\n          swap-size-gb: 8\n      - name: Update apt cache\n        run: >-\n          sudo apt-get update\n      - name: Install system dependencies\n        run: >-\n          sudo apt-get install -y\n          ninja-build\n          pkg-config\n          flex\n          bison\n          autoconf\n          autoconf-archive\n          automake\n          libtool\n      - name: Free Disk Space (Ubuntu)\n        if: ${{ !env.ACT }}\n        uses: jlumbroso/free-disk-space@main\n        with:\n          tool-cache: false\n          swap-storage: false\n      - name: Setup node from package.json\n        uses: actions/setup-node@v4\n        with:\n          node-version-file: \"package.json\"\n      - uses: pnpm/action-setup@v2\n      - name: Install dependencies\n        run: pnpm install\n      - name: Compile qgis-js\n        run: pnpm run compile:release\n      - name: Save logs on failure\n        if: ${{ failure() }}\n        uses: actions/upload-artifact@v4\n        with:\n          name: vcpkg-buildtrees-logs\n          path: build/vcpkg/buildtrees/**/*.log\n      - name: Build qgis-js packages\n        run: pnpm run build\n      - uses: actions/upload-artifact@v4\n        with:\n          name: \"package-qgis-js\"\n          path: |\n            packages/qgis-js/package.json\n            packages/qgis-js/README.md\n            packages/qgis-js/dist/**/*\n      - uses: actions/upload-artifact@v4\n        with:\n          name: \"package-qgis-js-utils\"\n          path: |\n            packages/qgis-js-utils/package.json\n            packages/qgis-js-utils/README.md\n            packages/qgis-js-utils/dist/**/*\n      - uses: actions/upload-artifact@v4\n        with:\n          name: \"package-qgis-js-ol\"\n          path: |\n            packages/qgis-js-ol/package.json\n            packages/qgis-js-ol/README.md\n            packages/qgis-js-ol/dist/**/*\n      - name: Build site\n        run: pnpm run dev:build\n      - name: Setup Pages\n        uses: actions/configure-pages@v4\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v3\n        with:\n          path: \"./sites/dev/dist\"\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".gitignore",
    "content": "package-lock.json\nCMakeLists.txt.user\nbuild/wasm\nnode_modules\ndist\npackages/*/dist\npackages/*/etc\nbin/act\ndocs/api\n.DS_Store\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"build/vcpkg\"]\n\tpath = build/vcpkg\n\turl = https://github.com/microsoft/vcpkg.git\n[submodule \"build/emsdk\"]\n\tpath = build/emsdk\n\turl = https://github.com/emscripten-core/emsdk.git\n"
  },
  {
    "path": ".nvmrc",
    "content": "v22.16.0\n"
  },
  {
    "path": ".prettierignore",
    "content": "public\ndist\nnode_modules\nbuild/wasm\nbuild/emsdk\nbuild/vcpkg\nbuild/vcpkg-ports\nbuild/vcpkg-toolchains\nbuild/vcpkg-triplets\nbuild/qt-patches\npnpm-lock.yaml\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{}\n"
  },
  {
    "path": ".vscode/c_cpp_properties.json",
    "content": "{\n  \"configurations\": [\n    {\n      \"name\": \"Linux\",\n      \"includePath\": [\n        \"${workspaceFolder}/build/wasm/vcpkg_installed/wasm32-emscripten-qt-threads/include\",\n        \"${workspaceFolder}/build/wasm/vcpkg_installed/wasm32-emscripten-qt-threads/include/qgis\",\n        \"${workspaceFolder}/build/emsdk/upstream/emscripten/system/include\"\n      ],\n      \"defines\": [],\n      \"compilerPath\": \"/usr/bin/gcc\",\n      \"cStandard\": \"c11\",\n      \"cppStandard\": \"c++17\",\n      \"intelliSenseMode\": \"clang-x64\"\n    }\n  ],\n  \"version\": 4\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"workbench.editor.enablePreview\": false,\n  \"editor.tabSize\": 2,\n  \"editor.formatOnSave\": true,\n  \"files.insertFinalNewline\": true,\n  \"files.associations\": {\n    \"**/package.json\": \"json\",\n    \"**/*.json\": \"jsonc\",\n    \"**/*.svg\": \"xml\",\n    \"**/*.cpp\": \"cpp\",\n    \"**/*.hpp\": \"cpp\",\n    \"functional\": \"cpp\"\n  },\n  \"terminal.integrated.env.linux\": {\n    \"PATH\": \"${workspaceRoot}/build/emsdk:${workspaceRoot}/build/emsdk/upstream/emscripten:${workspaceRoot}/build/emsdk/upstream/bin:${workspaceRoot}/build/vcpkg:${env:PATH}\"\n  },\n  \"search.exclude\": {\n    \"build/emsdk\": true,\n    \"build/vcpkg\": true,\n    \"docs/api\": true,\n    \"*/*/node_modules\": true,\n    \"*/*/dist\": true,\n    \"*/*/temp\": true,\n    \"*/*/.vitepress/dist\": true\n  },\n  \"typescript.tsdk\": \"node_modules/typescript/lib\",\n  \"C_Cpp.autoAddFileAssociations\": false,\n  \"C_Cpp.clang_format_path\": \"${workspaceRoot}/node_modules/clang-format/bin/linux_x64/clang-format\",\n  \"cmake.cmakePath\": \"${workspaceFolder}/build/vcpkg/downloads/tools/cmake-3.27.1-linux/cmake-3.27.1-linux-x86_64/bin/cmake\",\n  \"cmake.environment\": {\n    \"VCPKG_BINARY_SOURCES\": \"clear\"\n  },\n  \"cmake.configureSettings\": {\n    \"CMAKE_TOOLCHAIN_FILE\": \"${workspaceFolder}/build/vcpkg/scripts/buildsystems/vcpkg.cmake\",\n    \"VCPKG_CHAINLOAD_TOOLCHAIN_FILE\": \"${workspaceFolder}/build/vcpkg-toolchains/qgis-js.cmake\",\n    \"VCPKG_TARGET_TRIPLET\": \"wasm32-emscripten-qt-threads\",\n    \"VCPKG_OVERLAY_TRIPLETS\": \"${workspaceFolder}/build/vcpkg-triplets\",\n    \"VCPKG_OVERLAY_PORTS\": \"${workspaceFolder}/build/vcpkg-ports\"\n  },\n  \"cmake.buildDirectory\": \"${workspaceFolder}/build/wasm\",\n  \"cmake.emscriptenSearchDirs\": [\"build/emsdk/upstream/emscripten\"],\n  \"prettier.configPath\": \".prettierrc.json\",\n  \"prettier.documentSelectors\": [\n    \"**/*.{js,cjs,json,ts,vue,html,css,svg,yaml,md,webmanifest,clang-format,npmrc}\"\n  ],\n  \"clang-format.executable\": \"${workspaceRoot}/node_modules/clang-format/bin/linux_x64/clang-format\"\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "This document describes changes between tagged qgis-js versions\n\n## 4.1.0 (in development)\n\n- Replaced `mapLayers()` with `layerTreeRoot()`. (#25)\n  - Exposing the full layer tree hierarchy (groups, nested layers, visibility, expand/collapse).\n  - Use `layerTreeRoot().findLayers()` as a migration path for flat layer access.\n- Added `QgsMapLayer` and `QgsVectorLayer` wrappers with `name`, `opacity`, `id()`, `type()`, and `subsetString()`. (#59)\n- Added layer legend support. (#59)\n  - `QgsLayerTreeLayer.legendNodes()` returns individual legend entries with `label()` and `symbolImage()`.\n  - `renderLegend()` renders the full project legend as a high-DPI PNG (base64 data URL).\n  - `QgsLayerTreeGroup.renderLegend()` and `QgsLayerTreeLayer.renderLegend()` for subtree/single-layer legends.\n- Added optional `layerIds` parameter to all render functions (`renderImage`, `renderXYZTile`, `renderJob`, `fullExtent`, `renderLegend`). (#59)\n  - Enables rendering subsets of layers and composing multiple OL layers from different QGIS layer groups.\n  - All three OL data sources (`QgisCanvasDataSource`, `QgisXYZDataSource`, `QgisJobDataSource`) accept `layerIds` in options.\n- Added `loadLayerDefinition()` to load .qlr files at runtime into the project layer tree. (#59)\n\n## 4.0.0 (16. March 2026)\n\n- Major version bump to align with QGIS 4 (based on QGIS 4.0.0). (#39)\n  - Upstreamed patches to the QGIS repository for long-term maintainability.\n  - Made possible by the QGIS.org grant programme 2025.\n  - Special thanks to Matthias for the substantial contributions.\n- Dependency updates:\n  - Updated Qt to 6.10.1\n  - Updated emsdk to 5.0.2\n  - Updated Node to 22.16.0\n  - Updated Vite to 8.0.0\n  - Updated OpenLayers (\"ol\") to 10.8.0\n- Refactored `Rectangle` and `PointXY` types to `QgsRectangle` and `QgsPointXY`\n  across the codebase. This is a breaking API change. (#53)\n- Added API to get/set global and project variables.\n- Switched from pinned vcpkg to git submodule (based on QGIS).\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.15)\n\nproject(qgis-js CXX)\n\nset(CMAKE_CXX_STANDARD 17)\n\nfind_package(unofficial-sqlite3 CONFIG REQUIRED)\nfind_package(PROJ CONFIG REQUIRED)\nfind_package(GEOS CONFIG REQUIRED)\nfind_package(GDAL CONFIG REQUIRED)\nfind_package(expat CONFIG REQUIRED)\nfind_package(libzip CONFIG REQUIRED)\nfind_package(exiv2 CONFIG REQUIRED)\nfind_package(Protobuf CONFIG REQUIRED)\nfind_package(zstd CONFIG REQUIRED)\n\nfind_package(Qt6 REQUIRED COMPONENTS Core Gui Xml Network Concurrent Core5Compat PrintSupport Widgets)\n\nfind_package(Qt6Keychain CONFIG REQUIRED)\n\n# Set debug prefix early so all find_library calls use it\nif (CMAKE_BUILD_TYPE STREQUAL \"Debug\")\n  set(qgis_debug_prefix \"debug/\")\n  set(spatialindex_name \"spatialindexd\")\nelse()\n  set(qgis_debug_prefix \"\")\n  set(spatialindex_name \"spatialindex\")\nendif()\n\nfind_library(SPATIALINDEX_LIBRARY ${spatialindex_name} PATHS ${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/${qgis_debug_prefix}lib NO_DEFAULT_PATH REQUIRED)\n\nfind_path(QGIS_INCLUDE_DIR\n  NAMES qgis.h\n  PATHS \"${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/include\"\n  PATH_SUFFIXES qgis\n  NO_DEFAULT_PATH\n)\n\nfind_library(\n QGIS_LIBRARY\n NAMES qgis_core\n PATHS \"${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/${qgis_debug_prefix}lib\"\n PATH_SUFFIXES qgis\n NO_DEFAULT_PATH\n)\n\n# because on the initial build the Qt toolchain file is not yet generated and therefore not included by qgis-js.cmake\n# this will ensure that the Qt toolchain file is included after qtbase is built\nif(EXISTS ${QT_TOOLCHAIN_FILE})\n  set(QT_CHAINLOAD_TOOLCHAIN_FILE ${EMSCRIPTEN_TOOLCHAIN_FILE})\n  include(${QT_TOOLCHAIN_FILE})\nelse()\n  message(FATAL_ERROR \"Could not find Qt toolchain file: ${QT_TOOLCHAIN_FILE}\")\nendif()\n\n# since Qt 6.3 qt_standard_project_setup should be used so set some default values\nqt_standard_project_setup()\n\nset(QGISJS_SOURCES\n src/qgis-js.cpp\n src/api/QgisApi.cpp\n)\n\n# this creates also .html + qtloader.js + adds various flags to the build\nqt_add_executable(qgis-js MANUAL_FINALIZATION ${QGISJS_SOURCES})\n\n\ntarget_compile_options(qgis-js PRIVATE \"-fwasm-exceptions\")\ntarget_link_options(qgis-js PRIVATE \"-fwasm-exceptions\")\n\n# TODO reenable -msimd128\n# target_compile_options(qgis-js PRIVATE \"-msimd128\")\n# target_link_options(qgis-js PRIVATE \"-msimd128\")\n\ntarget_include_directories(qgis-js PRIVATE ${QGIS_INCLUDE_DIR})\n\ntarget_link_libraries(qgis-js PRIVATE\n  Qt6::Xml\n  Qt6::Concurrent\n  Qt6::Network\n  Qt6::Core\n  Qt6::Gui\n  Qt6::Core5Compat\n  Qt6::PrintSupport\n  Qt6::Widgets # because QgsApplication -> QApplication\n  )\n\nset(QGIS_PROVIDERS_LIST\n  provider_arcgisfeatureserver\n  provider_arcgismapserver\n  provider_delimitedtext\n  provider_wms\n  provider_wcs\n)\n\nforeach (provider ${QGIS_PROVIDERS_LIST})\n find_library(\n  QGIS_${provider}_LIBRARY\n  NAMES ${provider}_a\n  PATHS \"${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/${qgis_debug_prefix}lib\"\n  PATH_SUFFIXES qgis\n  NO_DEFAULT_PATH\n )\n target_link_libraries(qgis-js PRIVATE ${QGIS_${provider}_LIBRARY}\n )\nendforeach ()\n\n# qgis_core must come after providers (they depend on it)\ntarget_link_libraries(qgis-js PRIVATE ${QGIS_LIBRARY})\n\ntarget_link_libraries(qgis-js PRIVATE PROJ::proj)\ntarget_link_libraries(qgis-js PRIVATE unofficial::sqlite3::sqlite3)\ntarget_link_libraries(qgis-js PRIVATE GEOS::geos_c)\ntarget_link_libraries(qgis-js PRIVATE GDAL::GDAL)\ntarget_link_libraries(qgis-js PRIVATE expat::expat)\ntarget_link_libraries(qgis-js PRIVATE protobuf::libprotobuf-lite)\ntarget_link_libraries(qgis-js PRIVATE ${SPATIALINDEX_LIBRARY})\ntarget_link_libraries(qgis-js PRIVATE libzip::zip)\ntarget_link_libraries(qgis-js PRIVATE Qt6Keychain::Qt6Keychain)\ntarget_link_libraries(qgis-js PRIVATE $<IF:$<TARGET_EXISTS:zstd::libzstd_shared>,zstd::libzstd_shared,zstd::libzstd_static>)\n\nqt_finalize_executable(qgis-js)\n\n#\n# emcc settings (see https://emsettings.surma.technology/)\n#\n# NOTE: We set our flags after qt_finalize_executable in order to override the flags set by Qt\n#\n# Flags set by Qt:\n# -s PTHREAD_POOL_SIZE=4\n# -s INITIAL_MEMORY=50MB \n# -s EXPORTED_RUNTIME_METHODS=UTF16ToString,stringToUTF16,JSEvents,specialHTMLTargets,FS\n# -s MAX_WEBGL_VERSION=2\n# -s FETCH=1 \n# -s WASM_BIGINT=1\n# -s STACK_SIZE=5MB\n# -s MODULARIZE=1\n# -s EXPORT_NAME=createQtAppInstance\n# -s ALLOW_MEMORY_GROWTH\n#- s ASYNCIFY_IMPORTS=qt_asyncify_suspend_js,qt_asyncify_resume_js\n# -s ERROR_ON_UNDEFINED_SYMBOLS=1\n\n# Emscripten Runtime\ntarget_link_options(qgis-js PRIVATE \"SHELL: \\\n-s EXPORT_ES6\"\n)\n\n# FS (see https://emscripten.org/docs/api_reference/Filesystem-API.html)\ntarget_link_options(qgis-js PRIVATE \"SHELL: \\\n-s FORCE_FILESYSTEM=1\"\n)\n\n# Threading (see https://emscripten.org/docs/porting/pthreads.html)\nset(MINIMAL_THREAD_POOL_SIZE \"4\")\nset(MAXIMAL_THREAD_POOL_SIZE \"16\")\ntarget_link_options(qgis-js PRIVATE \"SHELL: \\\n-s PTHREAD_POOL_SIZE=\\\"Math.min(Math.max(((navigator&&navigator.hardwareConcurrency)||${MINIMAL_THREAD_POOL_SIZE}),${MINIMAL_THREAD_POOL_SIZE}),${MAXIMAL_THREAD_POOL_SIZE})\\\" \\\n-s PTHREAD_POOL_SIZE_STRICT=2 \\\n-s PTHREAD_POOL_DELAY_LOAD=1 \\\n-s MALLOC=mimalloc\"\n)\n\ntarget_link_options(qgis-js PRIVATE \"SHELL: \\\n--preload-file ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/share/proj@/proj \\\n--preload-file ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/share/qgis/resources/srs.db@/qgis/resources/srs.db \\\n--preload-file ${CMAKE_CURRENT_SOURCE_DIR}/src/qt.conf@/qt.conf\"\n)\n\n# Non-release builds: optimize link step to reduce function count (237K+ causes V8 OOM in Chrome)\n# This only affects the post-link wasm-opt pass, not source-level compilation\nif(NOT CMAKE_BUILD_TYPE STREQUAL \"Release\")\n  target_link_options(qgis-js PRIVATE \"-O2\")\nendif()\n\n# Debug build settings\nif (CMAKE_BUILD_TYPE STREQUAL \"Debug\")\n  target_compile_options(qgis-js PRIVATE \"-Og\")\n  # DWARF debug info (see https://developer.chrome.com/blog/faster-wasm-debugging/)\n  target_link_options(qgis-js PRIVATE \"-gseparate-dwarf\" \"-gdwarf-5\" \"-gsplit-dwarf\" \"-gpubnames\")\n  # Allow the wasm function table to grow dynamically (debug builds have more indirect call targets)\n  target_link_options(qgis-js PRIVATE \"SHELL:-s ALLOW_TABLE_GROWTH=1\")\nendif()\n\n# TODO remove this fix (see https://github.com/emscripten-core/emscripten/issues/21844)\ntarget_link_options(qgis-js PRIVATE \"SHELL: \\\n-s EXPORTED_FUNCTIONS=_main,__emscripten_thread_crashed,__embind_initialize_bindings\"\n)\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to qgis-js\n\nThank you for considering contributing to qgis-js! We welcome all contributions, big or small 🙏\n\n## QGIS Code of Conduct\n\nPlease note that for this project the [QGIS Code of Conduct](https://qgis.org/en/site/getinvolved/governance/codeofconduct/codeofconduct.html) also applies.\n\n## Getting Started\n\nTo get started with contributing, please follow these steps:\n\n1. Fork the repository and clone it to your local machine.\n2. See the [README](../README.md) for instructions on how to build the project.\n3. Make your changes and test them locally.\n4. Submit a pull request with your changes.\n\n## Code Style\n\nPlease follow the existing code style when making changes. We use [Prettier](https://prettier.io/) and [ClangFormat](https://clang.llvm.org/docs/ClangFormat.html) to enforce consitent code style, so please make sure your changes pass the linter by running `npm run lint`.\n\n## Reporting Bugs\n\nIf you find a bug, please open an issue on the [issue tracker](https://github.com/qgis/qgis-js/issues) with a detailed description of the problem and steps to reproduce it.\n\n## Contact\n\nIf you have any questions or concerns, please reach out as follows:\n\n- [qgis-js issues](https://github.com/qgis/qgis-js/issues)\n- [qgis-js discussions](https://github.com/qgis/qgis-js/discussions)\n- [QGIS Developers mailing list](https://lists.osgeo.org/mailman/listinfo/qgis-developer)\n\n> For general questions about QGIS, have a look at the [Get Involved in the QGIS Community](https://qgis.org/en/site/getinvolved/index.html) site\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\nIn addition, as a special exception, the QGIS Development Team gives\npermission to link the code of this program with the Qt library,\nincluding but not limited to the following versions (both free and\ncommercial): Qt/Non-commercial Windows, Qt/Windows, Qt/X11, Qt/Mac, and\nQt/Embedded (or with modified versions of Qt that use the same license\nas Qt), and distribute linked combinations including the two. You must\nobey the GNU General Public License in all respects for all of the code\nused other than Qt. If you modify this file, you may extend this\nexception to your version of the file, but you are not obligated to do\nso. If you do not wish to do so, delete this exception statement from\nyour version.\n\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  <signature of Ty Coon>, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.\n"
  },
  {
    "path": "README.md",
    "content": "# qgis-js\n\n**QGIS core ported to WebAssembly to run it on the web platform**\n\nVersion: `4.0.0` (based on QGIS 4.0.0)\n\n[qgis-js Repository](https://github.com/qgis/qgis-js) | [qgis-js Website](https://qgis.github.io/qgis-js)\n\n> ⚠️🧪 **Work in progress**! Currently this project is in public beta and only does very basic things like loading a QGIS project and rendering it to an image _(see [Features](#features) and [Limitations](#limitations))_\n\n> 🌱👋 **Help wanted**! Please try out your QGIS projects and report [issues](https://github.com/qgis/qgis-js/issues) and [ideas](https://github.com/qgis/qgis-js/discussions/categories/ideas) on GitHub. We are also warmly welcoming contributions to this project _(see [Contributing](#contributing))_\n\n## About\n\nThis project provides recipes to compile [QGIS](https://qgis.org/) core and its [dependencies](#libraries) to [WebAssembly](https://webassembly.org/) using [Emscripten](https://emscripten.org/), [CMake](https://cmake.org/) and [vcpkg](https://vcpkg.io).\n\nqgis-js provides a JavaScript/TypeScript API to interact with QGIS, load projects and render beautiful QGIS-based maps on the web platform (see [Features](#features)).\n\nPlease note that our focus is currently on making the QGIS core usable. The project does not aim to bring the full QGIS desktop application, GUI library, or Python bindings (see [Limitations](#limitations)).\n\n> 📚 See the [qgis-js Website](https://qgis.github.io/qgis-js) or [`./docs`](https://github.com/qgis/qgis-js/tree/main/docs) for more detailed information\n\n## Packages\n\n| Package                                                  | Description                                                           | npm                                                                                                                   |\n| -------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| **[qgis-js](./packages/qgis-js/README.md)**              | The qgis-js API (which also ships the `.wasm` binary)                 | [![qgis-js on npm](https://img.shields.io/npm/v/qgis-js)](https://www.npmjs.com/package/qgis-js)                      |\n| **[@qgis-js/ol](./packages/qgis-js-ol/README.md)**       | [OpenLayers](https://openlayers.org/) sources to display qgis-js maps | [![@qgis-js/ol on npm](https://img.shields.io/npm/v/@qgis-js/ol)](https://www.npmjs.com/package/@qgis-js/ol)          |\n| **[@qgis-js/utils](./packages/qgis-js-utils/README.md)** | Utilities to integrate qgis-js into web applications                  | [![@qgis-js/utils on npm](https://img.shields.io/npm/v/@qgis-js/utils)](https://www.npmjs.com/package/@qgis-js/utils) |\n\n## Getting started\n\n| Example                              | Source code                                                                | StackBlitz                                                                                                                                                                                                                                              |\n| ------------------------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| 📐 **Using the qgis-js API example** | [`docs/examples/qgis-js-example-api`](./docs/examples/qgis-js-example-api) | [![Open the \"Using the qgis-js API\" example in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/qgis/qgis-js/tree/main/docs/examples/qgis-js-example-api?file=main.js&title=qgis-js-example-api) |\n| 🗺️ **Minimal OpenLayers example**    | [`docs/examples/qgis-js-example-ol`](./docs/examples/qgis-js-example-ol)   | [![Open the \"Minimal OpenLayers\" example in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/qgis/qgis-js/tree/main/docs/examples/qgis-js-example-ol?file=main.js&title=qgis-js-example-ol)      |\n\n## Compatibility\n\nA modern desktop browser is needed. At the moment we only support/test **Chromium-based browsers (>= 95)** and **Firefox (>= 100)**\n\n> 📚 See [docs/compatibility.md](docs/compatibility.md) for more details\n\n## Features\n\nThis project is a work in progress. Currently it provides the following features:\n\n- QGIS core (and its [dependencies](#libraries)) compiled to WebAssembly\n  - JavaScript/TypeScript API to interact QGIS core\n- Loading of QGIS projects\n- Non-blocking rendering of QGIS maps/tiles to [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData?retiredLocale=de)\n- Optional [OpenLayers](https://openlayers.org/) integration\n\n## Limitations\n\nCompared to the native build of QGIS, there are various limitations:\n\n- The API surface is very limited at the moment\n- Network-based layers (e.g. WMS, WFS, WMTS, XYZ, COG, Vector Tiles) are not supported at the moment\n- No Python (PyQGIS) available\n- No Qt GUI provided\n- Some providers that need to communicate with a server using sockets will probably never work without proxies (e.g. PostgreSQL)\n\n## How to build qgis-js\n\n> 💡 NOTE: To just use qgis-js you don't need to build it yourself, you can install it from npm. See the provided [Packages](#packages).\n\n### Install dependencies\n\n#### Install the following **system packages** (on Ubuntu 22.04):\n\n```\nsudo apt-get install pkg-config ninja-build flex bison\n```\n\n#### Install dependencies with pnpm:\n\n```\nnpx pnpm install\n```\n\n> This will also invoke `./qgis-js.ts -v install` on \"postinstall\" which\n>\n> - downloads and installs emsdk in `build/emdsk`\n> - downloads and installs vcpkg in `build/vcpkg`\n> - boostraps vcpkg and downlaod the ports sources\n>\n> see also [`build/scripts/install.sh`](./build/scripts/install.sh) for manual installation\n\n### Compile qgis-js (and its [dependencies](#libraries)) with Emscripten\n\n```\nnpm run compile\n```\n\n> - Can also be ivoked with `compile:debug` or `compile:release`, see [Build types](#Build-types)\n> - Will take about 30 minutes on a modern machine to compile all the vcpkg ports during the first run... ☕\n> - see also [`build/scripts/compile.sh`](./build/scripts/compile.sh) for manual compiltion\n\n### Build `qgis-js` packages\n\nYou want to compile with a `Release` [build type](#build-types) first\n\n```\nnpm run compile:release\n```\n\nAfter successful compilation, you can build the packages with Vite:\n\n```\nnpm run build\n```\n\n> see the [packages listed at the beginning of this README](#packages)\n\n### Development\n\nYou probably want to compile with a `Dev` or `Debug` [build type](#build-types) first\n\n```\nnpm run compile:dev\n```\n\nStart a Vite development server:\n\n```\nnpm run dev\n```\n\nOpen your browser at http://localhost:5173\n\n## Libraries\n\n<!--NOTE: this can be generated with \"./qgis-js.ts libs -o markdown\"-->\n\n| Library                                                                                                                                                                                                                                                                                                                                                 | License                      | Links                                                                                                                       |\n| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |\n| **abseil** (20260107.1)<br /><div style=\"max-width:30em\">_Abseil is an open-source collection of C++ library code designed to augment the C++ standard library. The Abseil library code is collected from Google's own C++ code base, has been extensively tested and used in production, and is the same code we depend on in our daily coding lives._ | Apache-2.0                   | [Website](https://github.com/abseil/abseil-cpp) - [Source code](https://github.com/abseil/abseil-cpp)                       |\n| **double-conversion** (3.4.0)<br /><div style=\"max-width:30em\">_Efficient binary-decimal and decimal-binary conversion routines for IEEE doubles._                                                                                                                                                                                                      |                              | [Website](https://github.com/google/double-conversion) - [Source code](https://github.com/google/double-conversion)         |\n| **egl-registry** (2025-05-27)<br /><div style=\"max-width:30em\">_EGL API and Extension Registry_                                                                                                                                                                                                                                                         |                              | [Website](https://github.com/KhronosGroup/EGL-Registry) - [Source code](https://github.com/KhronosGroup/EGL-Registry)       |\n| **exiv2** (0.28.8)<br /><div style=\"max-width:30em\">_Image metadata library and tools_                                                                                                                                                                                                                                                                  | GPL-2.0-or-later             | [Website](https://exiv2.org) - [Source code](https://github.com/Exiv2/exiv2)                                                |\n| **expat** (2.7.4)<br /><div style=\"max-width:30em\">_XML parser library written in C_                                                                                                                                                                                                                                                                    | MIT                          | [Website](https://github.com/libexpat/libexpat) - [Source code](https://github.com/libexpat/libexpat)                       |\n| **freetype** (2.13.3)<br /><div style=\"max-width:30em\">_A library to render fonts._                                                                                                                                                                                                                                                                     | FTL OR GPL-2.0-or-later      | [Website](https://www.freetype.org/) - [Source code](https://gitlab.freedesktop.org/freetype/freetype)                      |\n| **gdal** (3.12.2)<br /><div style=\"max-width:30em\">_The Geographic Data Abstraction Library for reading and writing geospatial raster and vector data_                                                                                                                                                                                                  |                              | [Website](https://gdal.org) - [Source code](https://github.com/OSGeo/gdal)                                                  |\n| **geos** (3.14.1)<br /><div style=\"max-width:30em\">_Geometry Engine Open Source_                                                                                                                                                                                                                                                                        | LGPL-2.1-only                | [Website](https://libgeos.org/)                                                                                             |\n| **inih** (62)<br /><div style=\"max-width:30em\">_Simple .INI file parser_                                                                                                                                                                                                                                                                                | BSD-3-Clause                 | [Website](https://github.com/benhoyt/inih) - [Source code](https://github.com/benhoyt/inih)                                 |\n| **json-c** (0.18-20240915)<br /><div style=\"max-width:30em\">_A JSON implementation in C_                                                                                                                                                                                                                                                                | MIT                          | [Website](https://github.com/json-c/json-c) - [Source code](https://github.com/json-c/json-c)                               |\n| **libb2** (0.98.1)<br /><div style=\"max-width:30em\">_C library providing BLAKE2b, BLAKE2s, BLAKE2bp, BLAKE2sp_                                                                                                                                                                                                                                          |                              | [Website](https://github.com/BLAKE2/libb2) - [Source code](https://github.com/BLAKE2/libb2)                                 |\n| **libgeotiff** (1.7.4)<br /><div style=\"max-width:30em\">_Libgeotiff is an open source library on top of libtiff for reading and writing GeoTIFF information tags._                                                                                                                                                                                      | MIT                          | [Website](https://github.com/OSGeo/libgeotiff)                                                                              |\n| **libiconv** (1.18)<br /><div style=\"max-width:30em\">_iconv() text conversion._                                                                                                                                                                                                                                                                         |                              | [Website](https://www.gnu.org/software/libiconv/)                                                                           |\n| **libjpeg-turbo** (3.1.3)<br /><div style=\"max-width:30em\">_libjpeg-turbo is a JPEG image codec that uses SIMD instructions (MMX, SSE2, NEON, AltiVec) to accelerate baseline JPEG compression and decompression on x86, x86-64, ARM, and PowerPC systems._                                                                                             | BSD-3-Clause                 | [Website](https://github.com/libjpeg-turbo/libjpeg-turbo)                                                                   |\n| **liblzma** (5.8.2)<br /><div style=\"max-width:30em\">_Compression library with an API similar to that of zlib._                                                                                                                                                                                                                                         |                              | [Website](https://tukaani.org/xz/)                                                                                          |\n| **libpng** (1.6.55)<br /><div style=\"max-width:30em\">_libpng is a library implementing an interface for reading and writing PNG (Portable Network Graphics) format files_                                                                                                                                                                               | libpng-2.0                   | [Website](https://github.com/pnggroup/libpng)                                                                               |\n| **libspatialindex** (2.0.0)<br /><div style=\"max-width:30em\">_C++ implementation of R\\*-tree, an MVR-tree and a TPR-tree with C API._                                                                                                                                                                                                                   | MIT                          | [Website](http://libspatialindex.github.com)                                                                                |\n| **libzip** (1.11.4)<br /><div style=\"max-width:30em\">_A C library for reading, creating, and modifying zip archives._                                                                                                                                                                                                                                   | BSD-3-Clause                 | [Website](https://github.com/nih-at/libzip)                                                                                 |\n| **md4c** (0.5.2)<br /><div style=\"max-width:30em\">_MD4C is a C library providing a Markdown parser._                                                                                                                                                                                                                                                    | MIT                          | [Website](https://github.com/mity/md4c) - [Source code](https://github.com/mity/md4c)                                       |\n| **nlohmann-json** (3.12.0)<br /><div style=\"max-width:30em\">_JSON for Modern C++_                                                                                                                                                                                                                                                                       | MIT                          | [Website](https://github.com/nlohmann/json) - [Source code](https://github.com/nlohmann/json)                               |\n| **opengl-registry** (2026-01-26)<br /><div style=\"max-width:30em\">_OpenGL, OpenGL ES, and OpenGL ES-SC API and Extension Registry_                                                                                                                                                                                                                      |                              | [Website](https://github.com/KhronosGroup/OpenGL-Registry) - [Source code](https://github.com/KhronosGroup/OpenGL-Registry) |\n| **opengl** (2022-12-04)<br /><div style=\"max-width:30em\">_Open Graphics Library (OpenGL)[3][4][5] is a cross-language, cross-platform application programming interface (API) for rendering 2D and 3D vector graphics._                                                                                                                                 |                              |                                                                                                                             |\n| **pcre2** (10.47)<br /><div style=\"max-width:30em\">_Regular Expression pattern matching using the same syntax and semantics as Perl 5._                                                                                                                                                                                                                 | BSD-3-Clause                 | [Website](https://github.com/PCRE2Project/pcre2)                                                                            |\n| **proj** (9.7.1)<br /><div style=\"max-width:30em\">_PROJ library for cartographic projections_                                                                                                                                                                                                                                                           | MIT                          | [Website](https://proj.org/) - [Source code](https://github.com/OSGeo/PROJ)                                                 |\n| **protobuf** (6.33.4)<br /><div style=\"max-width:30em\">_Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data._                                                                                                                                                                                             | BSD-3-Clause                 | [Website](https://github.com/protocolbuffers/protobuf) - [Source code](https://github.com/protocolbuffers/protobuf)         |\n| **qgis** (4.0.0)<br /><div style=\"max-width:30em\">_QGIS is a free, open source, cross platform (lin/win/mac) geographical information system (GIS)_                                                                                                                                                                                                     | GPL-2.0                      | [Website](https://www.qgis.org/) - [Source code](https://github.com/qgis/QGIS)                                              |\n| **qt5compat** (6.10.1)<br /><div style=\"max-width:30em\">_The Qt 5 Core Compat module contains the Qt 5 Core APIs that were removed in Qt 6. The module facilitates the transition to Qt 6._                                                                                                                                                             |                              | [Website](https://www.qt.io/)                                                                                               |\n| **qtbase** (6.10.1)<br /><div style=\"max-width:30em\">_Qt Base (Core, Gui, Widgets, Network, ...)_                                                                                                                                                                                                                                                       |                              | [Website](https://www.qt.io/)                                                                                               |\n| **qtkeychain** (0.14.3)<br /><div style=\"max-width:30em\">_(Unaffiliated with Qt) Platform-independent Qt6 API for storing passwords securely_                                                                                                                                                                                                           | BSD-3-Clause                 | [Website](https://github.com/frankosterfeld/qtkeychain) - [Source code](https://github.com/frankosterfeld/qtkeychain)       |\n| **qtmultimedia** (6.10.1)<br /><div style=\"max-width:30em\">_Qt Multimedia is an add-on module that provides a rich set of QML types and C++ classes to handle multimedia content._                                                                                                                                                                      |                              | [Website](https://www.qt.io/)                                                                                               |\n| **qtshadertools** (6.10.1)<br /><div style=\"max-width:30em\">_The Qt Shader Tools module is designed to provide a set of tools and utilities to work with graphics shaders._                                                                                                                                                                             |                              | [Website](https://www.qt.io/)                                                                                               |\n| **qtsvg** (6.10.1)<br /><div style=\"max-width:30em\">_Qt SVG provides classes for rendering and displaying SVG drawings in widgets and on other paint devices._                                                                                                                                                                                          |                              | [Website](https://www.qt.io/)                                                                                               |\n| **qttools** (6.10.1)<br /><div style=\"max-width:30em\">_A collection of tools and utilities that come with the Qt framework to assist developers in the creation, management, and deployment of Qt applications._                                                                                                                                        |                              | [Website](https://www.qt.io/)                                                                                               |\n| **sqlite3** (3.51.2)<br /><div style=\"max-width:30em\">_SQLite is a software library that implements a self-contained, serverless, zero-configuration, transactional SQL database engine._                                                                                                                                                               | blessing                     | [Website](https://sqlite.org/)                                                                                              |\n| **tiff** (4.7.1)<br /><div style=\"max-width:30em\">_A library that supports the manipulation of TIFF image files_                                                                                                                                                                                                                                        | libtiff                      | [Website](https://libtiff.gitlab.io/libtiff/) - [Source code](https://gitlab.com/libtiff/libtiff)                           |\n| **utf8-range** (6.33.4)<br /><div style=\"max-width:30em\">_Fast UTF-8 validation with Range algorithm (NEON+SSE4+AVX2)_                                                                                                                                                                                                                                  | MIT                          | [Website](https://github.com/protocolbuffers/protobuf) - [Source code](https://github.com/protocolbuffers/protobuf)         |\n| **zlib** (1.3.1)<br /><div style=\"max-width:30em\">_A compression library_                                                                                                                                                                                                                                                                               | Zlib                         | [Website](https://www.zlib.net/) - [Source code](https://github.com/madler/zlib)                                            |\n| **zstd** (1.5.7)<br /><div style=\"max-width:30em\">_Zstandard - Fast real-time compression algorithm_                                                                                                                                                                                                                                                    | BSD-3-Clause OR GPL-2.0-only | [Website](https://facebook.github.io/zstd/) - [Source code](https://github.com/facebook/zstd)                               |\n\n## Build types\n\n### `Dev` build type\n\n- Optimized for **fast link times** during development\n  - Symbols are present (e.g. meaningful stack traces)\n  - Enables some Emscripten assertions\n  - No DWARF debug info\n- Empty `CMAKE_BUILD_TYPE` in CMake\n\n### `Debug` build type\n\n- Optimized for **debugging** with DWARF in Chromium-based browsers\n  - Includes symbols and DWARF debug info\n  - Enables most Emscripten assertions\n- see [docs/debugging.md](docs/debugging.md) on how to get started\n- Will take much longer to build than the default `Dev` build type\n- `CMAKE_BUILD_TYPE=Debug` in CMake\n\n### `Release` build type\n\n- Optimized for **performance and minimal package size**\n  - No symbols, assertions or DWARF debug info\n  - Minified JavaScript files\n- Will take much longer to build than the default `Dev` build type\n- `CMAKE_BUILD_TYPE=Release` in CMake\n\n## Contributing\n\nContributions welcome, see [CONTRIBUTING.md](CONTRIBUTING.md) for how to get started\n\n## License\n\n[GNU General Public License v2.0](LICENSE)\n"
  },
  {
    "path": "build/actions/clean.ts",
    "content": "import { CommandLineAction } from \"@rushstack/ts-command-line\";\n\nimport { QgisJsOptions } from \"./lib/QgisJsOptions\";\n\nimport \"zx/globals\";\n\nexport class CleanAction extends CommandLineAction {\n  private _options: QgisJsOptions;\n\n  public constructor(options: QgisJsOptions) {\n    super({\n      actionName: \"clean\",\n      summary: \"Clean qgis-js build tree\",\n      documentation: `Cleans emsdk, vcpkg and the qgis-js build tree in \"build/wasm\".`,\n    });\n    this._options = options;\n  }\n\n  protected onExecuteAsync(): Promise<void> {\n    return new Promise<void>(async (resolve) => {\n      const v = this._options.verbose;\n      $.verbose = this._options.verbose;\n\n      if (v) console.log(`- cleaning build/emsdk`);\n      await $`(cd build/emsdk && git clean -xfd)`;\n\n      if (v) console.log(`\\n- cleaning build/vcpkg`);\n      await $`(cd build/vcpkg && git clean -xfd)`;\n\n      if (v) console.log(`\\n- cleaning build/wasm`);\n      await $`(rm -rf build/wasm && mkdir build/wasm)`;\n\n      if (v) console.log(`\\n`);\n      resolve();\n    });\n  }\n}\n"
  },
  {
    "path": "build/actions/compile.ts",
    "content": "import { dirname, join } from \"path\";\nimport { fileURLToPath } from \"url\";\n\nimport {\n  CommandLineAction,\n  CommandLineChoiceParameter,\n  CommandLineFlagParameter,\n} from \"@rushstack/ts-command-line\";\n\nimport { BuildType } from \"./lib/BuildType\";\nimport { QgisJsOptions } from \"./lib/QgisJsOptions\";\n\nimport \"zx/globals\";\n\nconst CMakeCacheFile = \"build/wasm/CMakeCache.txt\";\n\nexport class CompileAction extends CommandLineAction {\n  private _options: QgisJsOptions;\n\n  private _buildType: CommandLineChoiceParameter;\n  private _debug: CommandLineFlagParameter;\n\n  public constructor(options: QgisJsOptions) {\n    super({\n      actionName: \"compile\",\n      summary: \"Compiles qgis-js\",\n      documentation:\n        \"Uses emsdk, vcpkg and CMake to configure and build qgis-js.\",\n    });\n    this._options = options;\n\n    this._debug = this.defineFlagParameter({\n      parameterLongName: \"--debug\",\n      parameterShortName: \"-d\",\n      description:\n        \"Prints debug information during build (and runs the compilation single threaded)\",\n    });\n\n    this._buildType = this.defineChoiceParameter({\n      parameterLongName: \"--builde-type\",\n      parameterShortName: \"-t\",\n      description: \"Specify the CMake build type\",\n      alternatives: [\"Dev\", \"Release\", \"Debug\"],\n      environmentVariable: \"QGIS_JS_BUILD_TYPE\",\n      defaultValue: \"Dev\" as BuildType,\n    });\n  }\n\n  protected onExecuteAsync(): Promise<void> {\n    return new Promise<void>(async (resolve, reject) => {\n      const v = this._options.verbose;\n      $.verbose = true;\n\n      const repo = join(dirname(fileURLToPath(import.meta.url)), \"../..\");\n\n      const buildType = (this._buildType.value || \"Dev\") as BuildType;\n      const debug = this._debug.value || false;\n\n      if (v) console.log(\"Build Type:\", buildType);\n\n      // check if CMakeCache.txt needs to be regenerated\n      if (fs.existsSync(CMakeCacheFile)) {\n        if (v)\n          console.log(\n            `\"${CMakeCacheFile}\" exists, checking if it has to be regenerated`,\n          );\n        const lastBuild = await lastBuildType();\n        if (lastBuild === buildType) {\n          if (v)\n            console.log(\n              `Build type has not changed, skipping regenerating of ${CMakeCacheFile}`,\n            );\n        } else {\n          if (v)\n            console.log(\n              `Build type has changed, regenerating ${CMakeCacheFile}`,\n            );\n          await $`rm ${CMakeCacheFile}`;\n\n          if (v)\n            console.log(`Build type has changed, removing build artifacts`);\n          const artifacts = await glob([\"build/wasm/{qt*,qgis-js*}\"]);\n          if (artifacts.length > 0) await $`rm ${artifacts}`;\n        }\n      } else {\n        if (v) console.log(`\"${CMakeCacheFile}\" does not exist`);\n      }\n\n      // set environment variables for CMake\n      process.env.VCPKG_BINARY_SOURCES = \"clear\";\n      if (debug) {\n        process.env.VERBOSE = \"1\";\n        process.env.EMCC_DEBUG = \"1\";\n      }\n\n      // if vcpkg has installed its own cmake, use that, otherwise use the system cmake\n      const cmake =\n        await $`find . -iwholename './build/vcpkg/downloads/tools/cmake-*/*/bin/cmake' | grep bin/cmake || echo cmake`;\n\n      // configure and build vcpgk dependencies\n      try {\n        await $`${cmake} \\\n-S . \\\n-B build/wasm \\\n-G Ninja \\\n-DCMAKE_TOOLCHAIN_FILE=${repo}/build/vcpkg/scripts/buildsystems/vcpkg.cmake \\\n-DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=${repo}/build/vcpkg-toolchains/qgis-js.cmake \\\n-DVCPKG_OVERLAY_TRIPLETS=./build/vcpkg-triplets \\\n-DVCPKG_OVERLAY_PORTS=./build/vcpkg-ports \\\n-DVCPKG_TARGET_TRIPLET=wasm32-emscripten-qt-threads \\\n-DCMAKE_BUILD_TYPE=${buildType !== \"Dev\" ? buildType : \"\"}`;\n\n        // build\n        await $`${cmake} --build build/wasm`;\n      } catch (error) {\n        reject(error);\n        return;\n      }\n\n      resolve();\n    });\n  }\n}\n\nasync function lastBuildType(): Promise<BuildType | undefined> {\n  const CMakeCacheFileContents = fs.readFileSync(CMakeCacheFile, \"utf-8\");\n  if (!CMakeCacheFileContents || CMakeCacheFileContents.length === 0)\n    return undefined;\n  const match = CMakeCacheFileContents.match(/CMAKE_BUILD_TYPE:STRING=(\\w+)/);\n  return (match ? match[1] : \"Dev\") as BuildType;\n}\n"
  },
  {
    "path": "build/actions/install.ts",
    "content": "import { CommandLineAction } from \"@rushstack/ts-command-line\";\n\nimport { QgisJsOptions } from \"./lib/QgisJsOptions\";\n\nimport \"zx/globals\";\n\nexport class InstallAction extends CommandLineAction {\n  private _options: QgisJsOptions;\n\n  public constructor(options: QgisJsOptions) {\n    super({\n      actionName: \"install\",\n      summary: \"Installs tools and downloads dependency sources\",\n      documentation: `Installs emsdk and vcpkg and then downlaods the source code of all ports with vcpkg.`,\n    });\n    this._options = options;\n  }\n\n  protected onExecuteAsync(): Promise<void> {\n    return new Promise<void>(async (resolve) => {\n      const v = this._options.verbose;\n      $.verbose = true;\n\n      if (v) console.log(`- installing emsdk`);\n      // ensure git submodule is initialized\n      await $`git submodule update --init build/emsdk`;\n      // read engine \"emsdk\" from package.json\n      const emsdkVersion = JSON.parse(fs.readFileSync(\"package.json\", \"utf-8\"))\n        .engines.emsdk;\n      if (!emsdkVersion || emsdkVersion === \"\")\n        throw new Error(`\"emsdk\" version not found in package.json`);\n      await $`(cd build/emsdk && ./emsdk install ${emsdkVersion} && ./emsdk activate ${emsdkVersion})`;\n\n      if (v) console.log(`\\n- installing vcpkg`);\n      // ensure git submodule is initialized\n      await $`git submodule update --init build/vcpkg`;\n      // bootstrap vcpkg\n      await $`./build/vcpkg/bootstrap-vcpkg.sh -disableMetrics`;\n\n      if (v) console.log(`\\n- running vcpkg install`);\n      await $`./build/vcpkg/vcpkg install \\\n--x-install-root=build/wasm/vcpkg_installed \\\n--only-downloads \\\n--overlay-triplets=build/vcpkg-triplets \\\n--overlay-ports=build/vcpkg-ports \\\n--triplet wasm32-emscripten-qt-threads`;\n\n      if (v) console.log(`\\n`);\n      resolve();\n    });\n  }\n}\n"
  },
  {
    "path": "build/actions/lib/BuildType.ts",
    "content": "export type BuildType = \"Dev\" | \"Release\" | \"Debug\";\n"
  },
  {
    "path": "build/actions/lib/QgisJsOptions.ts",
    "content": "export interface QgisJsOptions {\n  verbose: boolean;\n}\n"
  },
  {
    "path": "build/actions/libs.ts",
    "content": "import {\n  CommandLineAction,\n  CommandLineChoiceParameter,\n} from \"@rushstack/ts-command-line\";\n\nimport \"zx/globals\";\nimport { QgisJsOptions } from \"./lib/QgisJsOptions\";\n\nexport class LibsAction extends CommandLineAction {\n  private _options: QgisJsOptions;\n  private _output: CommandLineChoiceParameter;\n\n  public constructor(options: QgisJsOptions) {\n    super({\n      actionName: \"libs\",\n      summary: \"List the vcpkg libraries\",\n      documentation: `Generates a list of all vcpkg libraries with version and license.`,\n    });\n    this._options = options;\n\n    this._output = this.defineChoiceParameter({\n      parameterLongName: \"--output\",\n      parameterShortName: \"-o\",\n      description: \"Specify the commands output type\",\n      alternatives: [\"json\", \"markdown\"],\n      environmentVariable: \"QGIS_JS_LIBS_OUTPUT\",\n      defaultValue: \"json\",\n    });\n  }\n\n  protected onExecuteAsync(): Promise<void> {\n    return new Promise<void>(async (resolve) => {\n      $.verbose = this._options.verbose;\n\n      const vcpgkPortList = JSON.parse(\n        \"\" +\n          (await $`./build/vcpkg/vcpkg list \\\n      --x-install-root=build/wasm/vcpkg_installed \\\n      --overlay-triplets=build/vcpkg-triplets \\\n      --overlay-ports=build/vcpkg-ports \\\n      --triplet wasm32-emscripten-qt-threads \\\n      --x-full-desc \\\n      --x-json`),\n      );\n\n      const custom = {\n        qt6: {\n          license: \"LGPL-3.0\",\n          website: \"https://www.qt.io/\",\n          source: \"https://github.com/qt/qtbase\",\n        },\n        qgis: {\n          license: \"GPL-2.0\",\n          website: \"https://www.qgis.org/\",\n          source: \"https://github.com/qgis/QGIS\",\n        },\n        \"qca-qt6\": {\n          license: \"LGPL-2.1\",\n          website: \"https://userbase.kde.org/QCA\",\n          source: \"https://github.com/KDE/qca\",\n        },\n      } as any;\n\n      const libs = await Promise.all(\n        Object.entries(vcpgkPortList)\n          .filter(([key]) => key.endsWith(\"wasm32-emscripten-qt-threads\"))\n          .map<\n            Promise<{\n              name: string;\n              version: string;\n              description: string;\n              website?: string;\n              license?: string;\n              source?: string;\n            }>\n          >(\n            ([, value]) =>\n              new Promise(async (resolve) => {\n                const vcpgkPort = value as any;\n                const response = await fetch(\n                  `https://vcpkg.link/ports/${vcpgkPort[\"package_name\"]}.json`,\n                );\n                if (response.status === 200) {\n                  const vcpkgLink = await response.json();\n                  resolve({\n                    name: vcpgkPort[\"package_name\"].replace(/-qt6$/, \"\"),\n                    version: vcpgkPort[\"version\"],\n                    description: vcpgkPort[\"desc\"][0] || \"\",\n                    website: vcpkgLink[\"homepage_href\"],\n                    license: vcpkgLink[\"license\"],\n                    source: vcpkgLink[\"repository\"]?.[\"href\"],\n                  });\n                } else {\n                  if (vcpgkPort[\"package_name\"] in custom) {\n                    resolve({\n                      name: vcpgkPort[\"package_name\"].replace(/-qt6$/, \"\"),\n                      version: vcpgkPort[\"version\"],\n                      description: vcpgkPort[\"desc\"][0] || \"\",\n                      website: custom[vcpgkPort[\"package_name\"]][\"website\"],\n                      license: custom[vcpgkPort[\"package_name\"]][\"license\"],\n                      source: custom[vcpgkPort[\"package_name\"]][\"source\"],\n                    });\n                  } else {\n                    throw new Error(\"Could not find\", vcpgkPort);\n                  }\n                }\n              }),\n          ),\n      );\n      if (this._output.value === \"json\") {\n        console.log(JSON.stringify(libs, null, 2));\n      } else if (this._output.value === \"markdown\") {\n        const { tsMarkdown } = await import(\"ts-markdown\");\n        const rows = libs.map((lib) => ({\n          Library:\n            `**${lib.name}**` +\n            (lib.version ? ` (${lib.version})` : \"\") +\n            (lib.description\n              ? `<br /><div style=\"max-width:30em\">_${lib.description}_`\n              : \"\"),\n          License: lib.license || \"\",\n          Links:\n            [\n              ...(lib.website ? [`[Website](${lib.website})`] : []),\n              ...(lib.source ? [`[Source code](${lib.source})`] : []),\n            ].join(\" - \") || \"\",\n        }));\n        const table = {\n          table: {\n            columns: [\n              { name: \"Library\" },\n              { name: \"License\" },\n              { name: \"Links\" },\n            ],\n            rows,\n          },\n        };\n        console.log(tsMarkdown([table]));\n      }\n      resolve();\n    });\n  }\n}\n"
  },
  {
    "path": "build/actions/size.ts",
    "content": "import type { BrotliCompress, Gzip } from \"zlib\";\n\nimport {\n  CommandLineAction,\n  CommandLineChoiceParameter,\n} from \"@rushstack/ts-command-line\";\n\nimport { QgisJsOptions } from \"./lib/QgisJsOptions\";\n\nimport \"zx/globals\";\n\nexport class SizeAction extends CommandLineAction {\n  private _options: QgisJsOptions;\n  private _output: CommandLineChoiceParameter;\n\n  public constructor(options: QgisJsOptions) {\n    super({\n      actionName: \"size\",\n      summary: \"Measure the size of qgis-js\",\n      documentation: `Generates a list of all qgis-js assets and checks the compression ratio with \"gzip\" and \"brotli\".`,\n    });\n    this._options = options;\n\n    this._output = this.defineChoiceParameter({\n      parameterLongName: \"--output\",\n      parameterShortName: \"-o\",\n      description: \"Specify the commands output type\",\n      alternatives: [\"json\", \"markdown\"],\n      environmentVariable: \"QGIS_JS_SIZE_OUTPUT\",\n      defaultValue: \"json\",\n    });\n  }\n\n  protected onExecuteAsync(): Promise<void> {\n    return new Promise<void>(async (resolve) => {\n      $.verbose = this._options.verbose;\n\n      const fs = await import(\"fs\");\n      const zlib = await import(\"zlib\");\n\n      function measureCompression(\n        filePath: string,\n        compressionStream: BrotliCompress | Gzip,\n      ) {\n        return new Promise<number>((resolve, reject) => {\n          let compressedSize = 0;\n          const fileStream = fs.createReadStream(filePath);\n          fileStream.pipe(compressionStream);\n          compressionStream.on(\"data\", (chunk) => {\n            compressedSize += chunk.length;\n          });\n          compressionStream.on(\"end\", () => {\n            resolve(compressedSize);\n          });\n          fileStream.on(\"error\", reject);\n          compressionStream.on(\"error\", reject);\n        });\n      }\n\n      function humanFileSize(size: number) {\n        const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));\n        return (\n          Number((size / Math.pow(1024, i)).toFixed(2)) +\n          \" \" +\n          [\"B\", \"kB\", \"MB\", \"GB\", \"TB\"][i]\n        );\n      }\n\n      function spaceSaving(originalSize: number, compressedSize: number) {\n        return Math.round((1 - compressedSize / originalSize) * 100);\n      }\n\n      const distDir = \"packages/qgis-js/dist\";\n\n      const files = [\n        \"qgis.js\",\n        \"assets/wasm/qgis-js.js\",\n        \"assets/wasm/qgis-js.data\",\n        \"assets/wasm/qgis-js.wasm\",\n      ];\n\n      const filesSize = {} as {\n        [key: string]: {\n          bytes: number;\n          bytesGzip: number;\n          bytesBrotli: number;\n        };\n      };\n\n      for (const filename of files) {\n        const filePath = `${distDir}/${filename}`;\n        if (fs.existsSync(filePath)) {\n          const stat = fs.statSync(filePath);\n          filesSize[filename] = {\n            bytes: stat.size,\n            bytesGzip: await measureCompression(filePath, zlib.createGzip()),\n            bytesBrotli: await measureCompression(\n              filePath,\n              zlib.createBrotliCompress({\n                params: {\n                  // zlib.constants.BROTLI_DEFAULT_QUALITY is 11, which is too slow for stream compression,\n                  // so we use the defaul from Apache httpd which is 5\n                  // see https://httpd.apache.org/docs/2.4/mod/mod_brotli.html#brotlicompressionquality\n                  [zlib.constants.BROTLI_PARAM_QUALITY]: 5,\n                },\n              }),\n            ),\n          };\n        }\n      }\n\n      const total = Object.entries(filesSize).reduce(\n        (acc: any, [, value]) => {\n          acc.bytes += value.bytes;\n          acc.bytesGzip += value.bytesGzip;\n          acc.bytesBrotli += value.bytesBrotli;\n          return acc;\n        },\n        {\n          bytes: 0,\n          bytesGzip: 0,\n          bytesBrotli: 0,\n        },\n      );\n\n      if (this._output.value === \"json\") {\n        console.log(\n          JSON.stringify(\n            {\n              total,\n              files: filesSize,\n            },\n            null,\n            2,\n          ),\n        );\n      } else if (this._output.value === \"markdown\") {\n        const { tsMarkdown } = await import(\"ts-markdown\");\n\n        console.log(\n          `The size of the package is **\\`${humanFileSize(\n            total.bytes,\n          )}\\`** (uncompressed) or \\`${humanFileSize(\n            total.bytesBrotli,\n          )}\\` Brotli compressed (${spaceSaving(\n            total.bytes,\n            total.bytesBrotli,\n          )}% space saving) / \\`${humanFileSize(\n            total.bytesGzip,\n          )}\\` Gzip compressed (${spaceSaving(\n            total.bytes,\n            total.bytesGzip,\n          )}% space saving)`,\n        );\n        console.log(\"\");\n        console.log(\"It consists of the following files:\");\n        console.log(\"\");\n\n        const rows = Object.entries(filesSize).map(([file, size]) => ({\n          \"File name\": `\\`${file}\\``,\n          \"Size (uncompressed)\": `\\`${humanFileSize(size.bytes)}\\``,\n          \"Size (Brotli compressed)\": `\\`${humanFileSize(\n            size.bytesBrotli,\n          )}\\` (${spaceSaving(size.bytes, size.bytesBrotli)}% space saving)`,\n          \"Size (Gzip compressed)\": `\\`${humanFileSize(\n            size.bytesGzip,\n          )}\\` (${spaceSaving(size.bytes, size.bytesGzip)}% space saving)`,\n        }));\n        const table = {\n          table: {\n            columns: [\n              { name: \"File name\" },\n              { name: \"Size (uncompressed)\" },\n              { name: \"Size (Brotli compressed)\" },\n              { name: \"Size (Gzip compressed)\" },\n            ],\n            rows,\n          },\n        };\n        console.log(tsMarkdown([table]));\n      }\n      resolve();\n    });\n  }\n}\n"
  },
  {
    "path": "build/scripts/clean.sh",
    "content": "#!/bin/bash\nset -eo pipefail\n\n(cd build/emsdk; git clean -xfd)\n\n(cd build/vcpkg; git clean -xfd)\n\nrm -rf build/wasm && mkdir build/wasm\n"
  },
  {
    "path": "build/scripts/compile.sh",
    "content": "#!/bin/bash\nset -eo pipefail\n\n# if vcpkg has installed its own cmake, use that, otherwise use the system cmake\n$(find . -iwholename './build/vcpkg/downloads/tools/cmake-*/*/bin/cmake' | grep bin/cmake || echo cmake) \\\n-S . \\\n-B build/wasm \\\n-G Ninja \\\n-DCMAKE_TOOLCHAIN_FILE=$PWD/build/vcpkg/scripts/buildsystems/vcpkg.cmake \\\n-DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=$PWD/build/vcpkg-toolchains/qgis-js.cmake \\\n-DVCPKG_TARGET_TRIPLET=wasm32-emscripten-qt-threads \\\n-DCMAKE_BUILD_TYPE=\n\n$(echo ./build/vcpkg/downloads/tools/cmake-*/*/bin/cmake) \\\n--build build/wasm\n"
  },
  {
    "path": "build/scripts/install.sh",
    "content": "#!/bin/bash\nset -eo pipefail\n\ngit submodule update --init build/emsdk\nbuild/emsdk/emsdk install 5.0.2;\nbuild/emsdk/emsdk activate 5.0.2;\n\ngit submodule update --init build/vcpkg\n./build/vcpkg/bootstrap-vcpkg.sh -disableMetrics\n\n./build/vcpkg/vcpkg install \\\n--x-install-root=build/wasm/vcpkg_installed \\\n--only-downloads \\\n--triplet wasm32-emscripten-qt-threads\n"
  },
  {
    "path": "build/vcpkg-ports/libspatialindex/portfile.cmake",
    "content": "vcpkg_from_github(\n    OUT_SOURCE_PATH SOURCE_PATH\n    REPO libspatialindex/libspatialindex\n    REF \"${VERSION}\"\n    SHA512 a508a9ed4019641bdaaa53533505531f3db440b046a9c7d9f78ed480293200c51796c2d826a6bb9b4f9543d60bb0fef9e4c885ec3f09326cfa4d2fb81c1593aa\n    HEAD_REF master\n)\n\nvcpkg_cmake_configure(\n    SOURCE_PATH \"${SOURCE_PATH}\"\n    WINDOWS_USE_MSBUILD\n    OPTIONS\n        -DCMAKE_DEBUG_POSTFIX=d\n        -DSIDX_BUILD_TESTS:BOOL=OFF\n)\n\nvcpkg_cmake_install()\nvcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/${PORT})  \nvcpkg_fixup_pkgconfig()\nvcpkg_copy_pdbs()\n\n#Debug\nfile(REMOVE_RECURSE \"${CURRENT_PACKAGES_DIR}/debug/include\")\n\n# Handle copyright\nfile(INSTALL \"${SOURCE_PATH}/COPYING\" DESTINATION \"${CURRENT_PACKAGES_DIR}/share/${PORT}\" RENAME copyright)\n"
  },
  {
    "path": "build/vcpkg-ports/libspatialindex/vcpkg.json",
    "content": "{\n  \"name\": \"libspatialindex\",\n  \"version\": \"2.0.0\",\n  \"description\": \"C++ implementation of R*-tree, an MVR-tree and a TPR-tree with C API.\",\n  \"homepage\": \"http://libspatialindex.github.com\",\n  \"license\": \"MIT\",\n  \"dependencies\": [\n    {\n      \"name\": \"vcpkg-cmake\",\n      \"host\": true\n    },\n    {\n      \"name\": \"vcpkg-cmake-config\",\n      \"host\": true\n    },\n    \"zlib\"\n  ]\n}\n"
  },
  {
    "path": "build/vcpkg-ports/qgis/portfile.cmake",
    "content": "vcpkg_from_github(\n    OUT_SOURCE_PATH SOURCE_PATH\n    REPO qgis/QGIS\n    REF 8d368f3efd5be7a2321ada8cb5663d8f478f7d0f\n    SHA512 2d603d81ad2b37c809549ff4d05419adf7fb87e35500cdde3d8cda82b6cb8e5337552a4482b2a3a842fde81453383442bad93632070681e50502c12e57019036\n    HEAD_REF master\n)\n\nfile(REMOVE ${SOURCE_PATH}/cmake/FindQtKeychain.cmake)\nfile(REMOVE ${SOURCE_PATH}/cmake/FindGDAL.cmake)\nfile(REMOVE ${SOURCE_PATH}/cmake/FindGEOS.cmake)\nfile(REMOVE ${SOURCE_PATH}/cmake/FindEXIV2.cmake)\nfile(REMOVE ${SOURCE_PATH}/cmake/FindExpat.cmake)\nfile(REMOVE ${SOURCE_PATH}/cmake/FindIconv.cmake)\n\nvcpkg_find_acquire_program(FLEX)\nvcpkg_find_acquire_program(BISON)\nvcpkg_find_acquire_program(PYTHON3)\nget_filename_component(PYTHON3_PATH ${PYTHON3} DIRECTORY)\nvcpkg_add_to_path(${PYTHON3_PATH})\nvcpkg_add_to_path(${PYTHON3_PATH}/Scripts)\nset(PYTHON_EXECUTABLE ${PYTHON3})\n\n# Core options (matching build.sh / wasm.yml)\nlist(APPEND QGIS_OPTIONS -DFORCE_STATIC_LIBS:BOOL=ON)\nlist(APPEND QGIS_OPTIONS -DENABLE_TESTS:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DNATIVE_CRSSYNC_BIN=/bin/true)\n\n# Disabled features (matching build.sh / wasm.yml)\nlist(APPEND QGIS_OPTIONS -DWITH_GUI:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_DESKTOP:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_BINDINGS:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_3D:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_EXIV2:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_PDAL:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_DRACO:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_QTSERIALPORT:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_QTPOSITIONING:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_QTWEBENGINE:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_AUTH:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_SPATIALITE:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_ANALYSIS:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_QGIS_PROCESS:BOOL=OFF)\n\n# Additional disabled features for WASM/qgis-js\nlist(APPEND QGIS_OPTIONS -DWITH_POSTGRESQL:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_GRASS7:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_CUSTOM_WIDGETS:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_SERVER:BOOL=OFF)\n# Note: WITH_EPT and WITH_COPC are NOT disabled - they use WITH_INTERNAL_LAZPERF\nlist(APPEND QGIS_OPTIONS -DWITH_APIDOC:BOOL=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_QUICK:BOOL=OFF)\n\nlist(APPEND QGIS_OPTIONS -DWITH_INTERNAL_POLY2TRI=ON)\nlist(APPEND QGIS_OPTIONS -DWITH_INTERNAL_SPATIALINDEX=OFF)\nlist(APPEND QGIS_OPTIONS -DWITH_INTERNAL_LAZPERF=ON)\n\n# Point CMake to the installed Qt6, not the buildtrees\nlist(APPEND QGIS_OPTIONS -DQt6_DIR=${CURRENT_INSTALLED_DIR}/share/Qt6)\nlist(APPEND QGIS_OPTIONS -DCMAKE_PREFIX_PATH=${CURRENT_INSTALLED_DIR})\n\nlist(APPEND QGIS_OPTIONS -DQt6LinguistTools_DIR=${CURRENT_HOST_INSTALLED_DIR}/share/Qt6LinguistTools)\n\nlist(APPEND QGIS_OPTIONS -DQT_LRELEASE_EXECUTABLE=${CURRENT_HOST_INSTALLED_DIR}/tools/Qt6/bin/lrelease)\n\n# QGIS likes to install auth and providers to different locations on each platform\n# let's keep things clean and tidy and put them at a predictable location\nlist(APPEND QGIS_OPTIONS -DQGIS_PLUGIN_SUBDIR=lib)\n\n# By default QGIS installs includes into \"include\" on Windows and into \"include/qgis\" everywhere else\n# let's keep things clean and tidy and put them at a predictable location\nlist(APPEND QGIS_OPTIONS -DQGIS_INCLUDE_SUBDIR=include/qgis)\n\nlist(APPEND QGIS_OPTIONS -DWITH_INTERNAL_POLY2TRI=ON)\n\nvcpkg_configure_cmake(\n    SOURCE_PATH ${SOURCE_PATH}\n    #PREFER_NINJA\n    OPTIONS ${QGIS_OPTIONS}\n    OPTIONS_DEBUG ${QGIS_OPTIONS_DEBUG}\n    OPTIONS_RELEASE ${QGIS_OPTIONS_RELEASE}\n)\n\nvcpkg_install_cmake()\n\n\nfile(GLOB QGIS_CMAKE_PATH ${CURRENT_PACKAGES_DIR}/*.cmake)\nif(QGIS_CMAKE_PATH)\n    file(COPY ${QGIS_CMAKE_PATH} DESTINATION ${CURRENT_PACKAGES_DIR}/share/cmake/${PORT})\n    file(REMOVE_RECURSE ${QGIS_CMAKE_PATH})\nendif()\n\nfile(GLOB QGIS_CMAKE_PATH_DEBUG ${CURRENT_PACKAGES_DIR}/debug/*.cmake)\nif( QGIS_CMAKE_PATH_DEBUG )\n    file(REMOVE_RECURSE ${QGIS_CMAKE_PATH_DEBUG})\nendif()\n\nfile(REMOVE_RECURSE\n    ${CURRENT_PACKAGES_DIR}/debug/include\n)\nfile(REMOVE_RECURSE # Added for debug porpose\n    ${CURRENT_PACKAGES_DIR}/debug/share\n)\n\n# Handle copyright\nfile(INSTALL ${SOURCE_PATH}/COPYING DESTINATION ${CURRENT_PACKAGES_DIR}/share/${PORT} RENAME copyright)\n"
  },
  {
    "path": "build/vcpkg-ports/qgis/vcpkg.json",
    "content": "{\n  \"name\": \"qgis\",\n  \"version\": \"4.0.0\",\n  \"port-version\": 1,\n  \"homepage\": \"https://qgis.org\",\n  \"description\": \"QGIS is a free, open source, cross platform (lin/win/mac) geographical information system (GIS)\",\n  \"dependencies\": [\n    \"expat\",\n    \"zlib\",\n    \"zstd\",\n    \"libspatialindex\",\n    {\n      \"name\": \"sqlite3\",\n      \"default-features\": false\n    },\n    \"exiv2\",\n    \"protobuf\",\n    {\n      \"name\": \"proj\",\n      \"default-features\": false\n    },\n    \"geos\",\n    {\n      \"name\": \"gdal\",\n      \"default-features\": false\n    },\n    {\n      \"name\": \"qtbase\",\n      \"default-features\": false,\n      \"features\": [\n        \"concurrent\",\n        \"gui\",\n        \"jpeg\",\n        \"network\",\n        \"png\",\n        \"widgets\",\n        \"sql\",\n        \"sql-sqlite\"\n      ]\n    },\n    {\n      \"name\": \"qt5compat\",\n      \"default-features\": false\n    },\n    {\n      \"name\": \"qtkeychain-qt6\",\n      \"default-features\": false\n    },\n    {\n      \"name\": \"qtsvg\",\n      \"default-features\": false\n    },\n    {\n      \"name\": \"qtmultimedia\",\n      \"default-features\": false\n    },\n    {\n      \"name\": \"qttools\",\n      \"default-features\": false,\n      \"features\": [\n        \"linguist\"\n      ]\n    },\n    {\n      \"name\": \"qttools\",\n      \"host\": true,\n      \"default-features\": false,\n      \"features\": [\n        \"linguist\"\n      ]\n    },\n    {\n      \"name\": \"vcpkg-cmake\",\n      \"host\": true\n    },\n    {\n      \"name\": \"vcpkg-cmake-config\",\n      \"host\": true\n    }\n  ]\n}\n"
  },
  {
    "path": "build/vcpkg-toolchains/qgis-js.cmake",
    "content": "\nmessage(STATUS \"Using 'qgis-js' toolchain\")\n\n# Setup EMSDK/EMSCRIPTEN_ROOT\nif(NOT DEFINED ENV{EMSDK})\n  get_filename_component(QGIS_JS_BUILD_EMSDK \"${CMAKE_CURRENT_LIST_DIR}/../emsdk\" ABSOLUTE)\n  set(ENV{EMSDK} ${QGIS_JS_BUILD_EMSDK})\nendif()\nif(NOT EMSCRIPTEN_ROOT)\n   if(NOT DEFINED ENV{EMSDK})\n      message(FATAL_ERROR \"emsdk not found\")\n   endif()\n   set(EMSCRIPTEN_ROOT \"$ENV{EMSDK}/upstream/emscripten\")\nendif()\nif(NOT DEFINED ENV{EMSCRIPTEN_ROOT})\n  set(ENV{EMSCRIPTEN_ROOT} ${EMSCRIPTEN_ROOT})\nendif()\n\n# Set EMSCRIPTEN_TOOLCHAIN_FILE for use by CMakeLists.txt\nset(EMSCRIPTEN_TOOLCHAIN_FILE \"${EMSCRIPTEN_ROOT}/cmake/Modules/Platform/Emscripten.cmake\")\n\n# Include Emscripten toolchain directly\ninclude(\"${EMSCRIPTEN_TOOLCHAIN_FILE}\")\n\n# Set QT_TOOLCHAIN_FILE path for CMakeLists.txt to include\nif(CMAKE_TOOLCHAIN_FILE)\n  get_filename_component(VCPKG_ROOT_DIR ${CMAKE_TOOLCHAIN_FILE} DIRECTORY)\n  get_filename_component(VCPKG_ROOT_DIR ${VCPKG_ROOT_DIR} DIRECTORY)\n  get_filename_component(VCPKG_PACKAGES_PATH \"${VCPKG_ROOT_DIR}/../packages\" ABSOLUTE)\n  set(QT_TOOLCHAIN_FILE \"${VCPKG_PACKAGES_PATH}/qtbase_${VCPKG_TARGET_TRIPLET}/share/Qt6/qt.toolchain.cmake\")\nendif()\n\n# Flags for all ports and qgis-js\n# TODO reenable -msimd128\nset(QGIS_JS_FLAGS \"-pthread -fwasm-exceptions -sSUPPORT_LONGJMP=wasm\")\nset(CMAKE_C_FLAGS_INIT   \"${CMAKE_C_FLAGS_INIT} ${QGIS_JS_FLAGS}\")\nset(CMAKE_CXX_FLAGS_INIT \"${CMAKE_CXX_FLAGS_INIT} ${QGIS_JS_FLAGS}\")\n"
  },
  {
    "path": "build/vcpkg-triplets/wasm32-emscripten-qt-threads.cmake",
    "content": "message(STATUS \"Using 'wasm32-emscripten-qt-threads' triplet\")\n\nset(VCPKG_TARGET_ARCHITECTURE wasm32)\nset(VCPKG_CRT_LINKAGE dynamic)\nset(VCPKG_LIBRARY_LINKAGE static)\nset(VCPKG_CMAKE_SYSTEM_NAME Emscripten)\n\n# to avoid building both debug and release of all libs uncomment the following line\n# set(VCPKG_BUILD_TYPE \"release\")\n\nset(VCPKG_ENV_PASSTHROUGH_UNTRACKED EMSDK EMSCRIPTEN EMSCRIPTEN_ROOT PATH)\n\n# Tell autoconf configure scripts this is cross-compilation (fixes error 77)\nset(VCPKG_MAKE_BUILD_TRIPLET \"--host=wasm32-unknown-emscripten\")\n\n# this needs to be present for vcpkg installs, but also the same VCPKG_CHAINLOAD_TOOLCHAIN_FILE\n# needs to be present when running CMake so that the project gets it\nget_filename_component(QGISJS_TOOLCHAIN_FILE\n   \"${CMAKE_CURRENT_LIST_DIR}/../vcpkg-toolchains/qgis-js.cmake\"\n   ABSOLUTE)\nset(VCPKG_CHAINLOAD_TOOLCHAIN_FILE ${QGISJS_TOOLCHAIN_FILE})\n\nset(VCPKG_ENV_PASSTHROUGH_UNTRACKED EMSDK EMSCRIPTEN EMSCRIPTEN_ROOT PATH)\n\n"
  },
  {
    "path": "build/vite/CrossOriginIsolationPlugin.ts",
    "content": "import type { Plugin } from \"vite\";\n\nexport const CrossOriginIsolationResponseHeaders = {\n  \"Cross-Origin-Opener-Policy\": \"same-origin\",\n  \"Cross-Origin-Embedder-Policy\": \"require-corp\",\n};\n\n/**\n * A Vite plugin that adds Cross-Origin Isolation headers to the server responses.\n */\nexport default function CrossOriginIsolationPlugin(): Plugin {\n  return {\n    name: \"CrossOriginIsolationPlugin\",\n    configureServer(server) {\n      server.middlewares.use((_req, res, next) => {\n        Object.entries(CrossOriginIsolationResponseHeaders).forEach(\n          ([key, value]) => {\n            res.setHeader(key, value);\n          },\n        );\n        next();\n      });\n    },\n  };\n}\n"
  },
  {
    "path": "build/vite/DirectoryListingPlugin.ts",
    "content": "import { File, Folder } from \"../../packages/qgis-js-utils\";\n\nimport type { Plugin, ResolvedConfig } from \"vite\";\n\nimport { basename, resolve, join } from \"path\";\nimport { readdir } from \"fs/promises\";\nimport { Dirent } from \"fs\";\n\nconst FILTER_LIST = [\".DS_Store\", \".git\", \".gitignore\", \".env\"];\nconst DIRECTORY_LISTING_FILENAME = \"directory-listing.json\";\n\nlet config: ResolvedConfig;\n\nexport default function DirectoryListingPlugin(\n  directories: string | string[],\n): Plugin {\n  const directoriesToList =\n    typeof directories === \"string\" ? [directories] : directories;\n  for (const directory of directoriesToList) {\n    if (!directory.startsWith(\"public/\")) {\n      throw new Error(\n        `DirectoryListingPlugin: Directory ${directory} is not in the public folder`,\n      );\n    }\n  }\n  return {\n    name: \"DirectoryListingPlugin\",\n    configResolved(_config) {\n      config = _config;\n    },\n    configureServer(server) {\n      const dirs = directoriesToList.map(\n        (directory) =>\n          config.base +\n          directory.replace(/^public\\//, \"\") +\n          `/${DIRECTORY_LISTING_FILENAME}`,\n      );\n      server.middlewares.use(async (req, res, next) => {\n        if (req.url && dirs.includes(req.url)) {\n          let dir = req.url;\n          // remove potential base from start of the url\n          if (dir.startsWith(config.base)) {\n            dir = dir.slice(config.base.length);\n          }\n          // remove filename from url at the end\n          dir = dir.slice(0, -`/${DIRECTORY_LISTING_FILENAME}`.length);\n\n          const dirParts = dir.split(\"/\");\n          const dirBase = dirParts.slice(0, -1).join(\"/\");\n          const dirName = dirParts[dirParts.length - 1];\n\n          const drectoryListing = await createDirectoryListing(\n            join(process.cwd(), \"public\", dirBase),\n            dirName,\n            \".\",\n          );\n          res.setHeader(\"Content-Type\", \"application/json\");\n          res.end(JSON.stringify(drectoryListing, null, 2));\n        } else {\n          next();\n        }\n      });\n    },\n    async generateBundle() {\n      for (const dir of directoriesToList.map((directory) =>\n        directory.replace(/^public\\//, \"\"),\n      )) {\n        const drectoryListing = await createDirectoryListing(\n          join(process.cwd(), \"public\"),\n          dir,\n          \".\",\n        );\n        this.emitFile({\n          type: \"asset\",\n          fileName: dir + `/${DIRECTORY_LISTING_FILENAME}`,\n          source: JSON.stringify(drectoryListing, null, 2),\n        });\n      }\n    },\n  };\n}\n\nasync function readFolder(folder: string): Promise<Dirent[]> {\n  return await readdir(folder, { withFileTypes: true });\n}\n\nasync function createDirectoryListing(\n  root: string,\n  base: string,\n  directory: string,\n) {\n  const directoryListing = await readFolder(join(root, base, directory));\n  return directoryListing\n    .filter((dirent) => dirent.isFile || dirent.isDirectory)\n    .filter((dirent) => !FILTER_LIST.includes(dirent.name))\n    .reduce(\n      async (currentFolderPromise, entry: Dirent) => {\n        const currentFolder = (await currentFolderPromise) as Folder;\n        if (entry.isDirectory()) {\n          currentFolder.entries.push(\n            await createDirectoryListing(\n              root,\n              join(base, directory),\n              entry.name,\n            ),\n          );\n        } else {\n          currentFolder.entries.push({\n            name: entry.name,\n            path: join(currentFolder.path, entry.name),\n            type: \"File\",\n          } as File);\n        }\n        return currentFolder;\n      },\n      Promise.resolve({\n        name: basename(resolve(join(base, directory))),\n        path: join(base, directory),\n        type: \"Folder\",\n        entries: [],\n      } as Folder),\n    );\n}\n"
  },
  {
    "path": "build/vite/QgisRuntimePlugin.ts",
    "content": "import { join, resolve, basename } from \"path\";\nimport { existsSync, readFileSync } from \"fs\";\nimport { dirname } from \"path\";\nimport { fileURLToPath } from \"url\";\n\nimport { CrossOriginIsolationResponseHeaders } from \"./CrossOriginIsolationPlugin\";\n\nimport type { Plugin, ResolvedConfig } from \"vite\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nconst RUNTIME_JS = \"js\";\nconst RUNTIME_WASM = \"wasm\";\nconst RUNTIME_WASM_MAP = \"wasm.map\";\nconst RUNTIME_WASM_DEBUG = \"wasm.debug.wasm\";\nconst RUNTIME_DATA = \"data\";\n\nexport const BASE_DIR = \"wasm\";\n\nexport interface Runtime {\n  name: string;\n  outputDir: string;\n}\n\nfunction patchEmccJs(content: string): string {\n  // prevent setting of read-only property \"stack\" in Firefox\n  // (will throw an error in strict mode)\n  const search = `e.stack = arr.join(\"\\\\n\");`;\n  return content.replaceAll(\n    search,\n    `if (! (navigator.userAgent.indexOf(\"Firefox\") !== -1)) { ${search} }`,\n  );\n}\n\nexport default function QgisRuntimePlugin(_runtime: Runtime | null): Plugin {\n  let runtime: Runtime;\n  if (_runtime === null) {\n    throw new Error(\"QgisRuntimePlugin: No runtime specified\");\n  } else {\n    runtime = _runtime;\n  }\n\n  let config: ResolvedConfig;\n  let runtimeDir = () => `${config.build.assetsDir}/${BASE_DIR}`;\n\n  const repoRoot = resolve(__dirname, \"../..\");\n\n  function file(ending: string) {\n    return `${runtime.name}.${ending}`;\n  }\n\n  const fileRuntimeJs = file(RUNTIME_JS);\n  const fileRuntimeWasm = file(RUNTIME_WASM);\n  const fileRuntimeWasmMap = file(RUNTIME_WASM_MAP);\n  const fileRuntimeWasmDebug = file(RUNTIME_WASM_DEBUG);\n  const fileRuntimeData = file(RUNTIME_DATA);\n\n  const filesRuntime = [\n    fileRuntimeJs,\n    fileRuntimeWasmMap,\n    fileRuntimeWasmDebug,\n    fileRuntimeWasm,\n    fileRuntimeData,\n  ];\n\n  return {\n    name: \"QgisRuntimePlugin\",\n    enforce: \"pre\",\n    configResolved(_config) {\n      config = _config;\n    },\n    configureServer(server) {\n      const filesRuntimeDir = runtimeDir();\n      const runtimeFiles = filesRuntime.map(\n        (id) =>\n          `${config.base}${filesRuntimeDir ? filesRuntimeDir + \"/\" : \"\"}${id}`,\n      );\n      server.middlewares.use((req, res, next) => {\n        if (\n          req.url &&\n          runtimeFiles.some((runtimefile) => runtimefile === req.url)\n        ) {\n          const filePath = join(repoRoot, runtime.outputDir, basename(req.url));\n          if (existsSync(filePath)) {\n            const raw = readFileSync(filePath);\n            res.statusCode = 200;\n            Object.entries(CrossOriginIsolationResponseHeaders).forEach(\n              ([key, value]) => {\n                res.setHeader(key, value);\n              },\n            );\n            if (\n              filePath.endsWith(\".\" + RUNTIME_WASM) ||\n              filePath.endsWith(\".\" + RUNTIME_DATA)\n            ) {\n              if (filePath.endsWith(\".\" + RUNTIME_WASM)) {\n                res.setHeader(\"Content-Type\", \"application/wasm\");\n              }\n              res.end(raw);\n            } else if (filePath.endsWith(\".\" + RUNTIME_JS)) {\n              const content = raw.toString();\n              res.setHeader(\"Content-Type\", \"application/javascript\");\n              res.end(patchEmccJs(content));\n            } else if (filePath.endsWith(\".\" + RUNTIME_WASM_MAP)) {\n              const content = raw.toString();\n              res.setHeader(\"Content-Type\", \"application/json\");\n              res.end(content);\n            }\n          } else {\n            console.log(\"404\", filePath);\n            res.statusCode = 404;\n            res.end();\n          }\n        } else {\n          next();\n        }\n      });\n    },\n    generateBundle() {\n      filesRuntime.forEach((id) => {\n        const filesRuntimeDir = runtimeDir();\n        const filePath = join(repoRoot, runtime.outputDir, id);\n        if (existsSync(filePath)) {\n          const raw = readFileSync(filePath);\n\n          let source = raw as Uint8Array;\n\n          if (filePath.endsWith(\".\" + RUNTIME_JS)) {\n            const encoder = new TextEncoder();\n            const content = patchEmccJs(raw.toString());\n            source = encoder.encode(content);\n          }\n\n          this.emitFile({\n            fileName: `${filesRuntimeDir ? filesRuntimeDir + \"/\" : \"\"}${id}`,\n            type: \"asset\",\n            source,\n          });\n        }\n      });\n    },\n  };\n}\n"
  },
  {
    "path": "docs/architecture.md",
    "content": "# Architecture\n\n## qgis-js Repository\n\n## qgis-js Build\n\n## qgis-js Runtime\n"
  },
  {
    "path": "docs/bundling.md",
    "content": "# Bundling\n\nqgis-js consists of two parts: The runtime generated by Emscripten and the TypeScript/JavaScript API, that can be seen as a wrapper around the runtime. The wrapper can be imported, used and bundled (e.g. tree-shaked) like any other JavaScript library. But it is important that the runtime is not modified and served as is.\n\nSee the [`qgis-js` Package `README.md`](../packages/qgis-js/README.md) for more information about the files involved. Everything inside `assets/wasm` is part of the runtime.\n\nTo not confuse any downstream bundler, the runtime is [dynamically loaded](../packages/qgis-js/src/loader.ts) in a way that it will not be processed. Therefore **it is up to the end user to include the runtime files in the final build**.\n\n### Explicitly specifying the runtime location\n\nThe runtime location can be specified with the `prefix` configuration option. This is useful when the runtime is not in the same directory as the main script or served from a different server (e.g. a CDN).\n\n```js\nconst { api } = await qgis({\n  prefix: \"/path/to/runtime/assets\",\n});\n```\n\n## Examples\n\n### qgis-js with [Vite](https://vitejs.dev/)\n\nOne can use the [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy).\n\nFor an example see the [`vite.config.ts`](./examples/qgis-js-example-ol/vite.config.js) in the [qgis-js-example-ol](./examples/qgis-js-example-ol) project and note that the COOP/COEP headers have to be set after the plugin (see [`compatibility.md`](./compatibility.md) for more information).\n\n### qgis.js with [Webpack](https://webpack.js.org/)\n\nWith Webpack one can use the [copy-webpack-plugin](https://www.npmjs.com/package/copy-webpack-plugin).\n\nNote that the COOP/COEP headers have to be set in the `webpack.config.js` (see [`compatibility.md`](./compatibility.md) for more information).\n\n### Using qgis-js from a CDN\n\nAn example of how to use qgis-js from a CDN (e.g. [jsDelivr](https://www.jsdelivr.com/)):\n\n```html\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <title>qgis-js</title>\n  </head>\n  <body>\n    <script type=\"module\">\n      // import qgis-js from a CDN\n      const { qgis } =\n        await import(\"https://cdn.jsdelivr.net/npm/qgis-js/dist/qgis.js\");\n      // boot the qgis-js runtime\n      const { api } = await qgis();\n      // use the qgis-js api\n      const rect = new api.QgsRectangle(1, 1, 42, 42);\n      const center = rect.center();\n      console.log(`Center: x: ${center.x}, y: ${center.y}`);\n    </script>\n  </body>\n</html>\n```\n\nNote that the main script has to be explicitly loaded with `qgis-js/dist/qgis.js` (Or a prefix pointing to `qgis-js/dist/assets/wasm` has to be set ([see above](#explicitly-specifying-the-runtime-location))).\n\nAnd also ensure that the HTML document has the correct COOP/COEP headers set (see [`compatibility.md`](./compatibility.md) for more information).\n"
  },
  {
    "path": "docs/ci.md",
    "content": "# CI/CD\n"
  },
  {
    "path": "docs/compatibility.md",
    "content": "# Compatibility\n\n## Features\n\nqgis-js uses the following features which have to be supported by the JavaScript/WebAssembly runtime:\n\n- ES modules with [dynamic imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports)\n- [SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer)\n- [WebAssembly](https://developer.mozilla.org/en-US/docs/WebAssembly)\n  - [WebAssembly Threads](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer)\n  - [WebAssembly Exception Handling](https://developer.mozilla.org/en-US/docs/WebAssembly/Exception_handling)\n\n## COOP/COEP\n\nIn order to use SharedArrayBuffer a secure cross-origin context is required. This means that the [Cross-Origin-Opener-Policy (COOP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin-Opener-Policy) and [Cross-Origin-Embedder-Policy (COEP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin-Embedder-Policy) headers have to be set by the server.\n\nAlternativley one can use [coi-serviceworker](https://github.com/gzuidhof/coi-serviceworker) to set the headers through a service worker. This makes it possible to use qgis-js for example on GitHub pages.\n\n## Supported Browsers\n\nThe [features listed above are supported by the following browsers](https://caniuse.com/es6-module-dynamic-import,wasm,sharedarraybuffer,mdn-javascript_builtins_webassembly_exception,webworkers):\n\n- **Chromium based browsers (>= 95)**\n\n- **Firefox (>= 100)**\n\n- Safari (15.2, yet to be tested!)\n\n### Mobile Browsers\n\n> 🚧 At the moment we don't support mobile browsers\n\n## Supported JavaScript Runtimes\n\n> 🚧 At the moment only browsers are supported\n"
  },
  {
    "path": "docs/debugging.md",
    "content": "# Debugging\n\n## Setup\n\n- Make sure that you have compiled qgis-js with the [`Debug` build type](../README.md#build-types)\n\n- DWARF debug info is only supported in Chromium based browsers (Chrome, Edge, Brave, ...)\n  - Make sure that you have enabled the \"Experimental WebAssembly features\" flag in your browser (see [Debugging WebAssembly with modern tools](https://developer.chrome.com/blog/wasm-debugging-2020/))\n\n  - Install the [C/C++ DevTools Support (DWARF)](https://chrome.google.com/webstore/detail/pdcpmagijalfljmkmjngeonclgbbannb) extension\n\n## Links\n\n- [Debugging WebAssembly with modern tools](https://developer.chrome.com/blog/wasm-debugging-2020/), Chrome for Developers Blog, 2020\n  - [Debugging WebAssembly Faster](https://developer.chrome.com/blog/faster-wasm-debugging/), Chrome for Developers Blog, 2022\n\n- [WASM Debugging with Emscripten and VSCode](https://floooh.github.io/2023/11/11/emscripten-ide.html),\n  The Brain Dump, 2023\n"
  },
  {
    "path": "docs/examples/qgis-js-example-api/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>qgis-js-example-api</title>\n    <link rel=\"icon\" href=\"data:,\" />\n    <style>\n      body {\n        font-family: sans-serif;\n        margin: 1em;\n        padding: 1em;\n      }\n    </style>\n  </head>\n  <body>\n    <h2>📐 Using the qgis-js API example</h2>\n    <p>Open the Console to see the result 🤓</p>\n    <script type=\"module\" src=\"main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "docs/examples/qgis-js-example-api/main.js",
    "content": "const QGIS_JS_DIST = window.location.pathname + \"node_modules/qgis-js/dist\";\n\n// loading the qgis-js library\nconst { qgis, QGIS_JS_VERSION } = await import(QGIS_JS_DIST + \"/qgis.js\");\nconsole.log(`qgis-js (v${QGIS_JS_VERSION})`);\n\n// booting the qgis-js runtime\nconsole.log(`- loading qgis-js`);\nconst { api } = await qgis({\n  prefix: QGIS_JS_DIST + \"/assets/wasm\",\n});\nconsole.log(`- qgis-js ready`);\n\n// qgis-js API example\nconsole.log(`- creating a rectangle`);\nconst rect = new api.QgsRectangle(1, 2, 3, 4);\nconsole.log(\"-> \" + printRect(rect));\n\nconsole.log(`- scaling the rectangle`);\nrect.scale(5);\nconsole.log(\"-> \" + printRect(rect));\n\nconsole.log(`- getting the center of the rectangle`);\nconst center = rect.center();\nconsole.log(`-> Point: x: ${center.x}, y: ${center.y}`);\n\nfunction printRect(rect) {\n  return `QgsRectangle: xMaximum: ${rect.xMaximum}, xMinimum: ${rect.xMinimum}, yMaximum: ${rect.yMaximum}, yMinimum: ${rect.yMinimum}`;\n}\n"
  },
  {
    "path": "docs/examples/qgis-js-example-api/package.json",
    "content": "{\n  \"name\": \"qgis-js-example-api\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"start\": \"vite preview --outDir .\"\n  },\n  \"dependencies\": {\n    \"qgis-js\": \"4.0.0\"\n  },\n  \"devDependencies\": {\n    \"vite\": \"^8.0.0\"\n  }\n}\n"
  },
  {
    "path": "docs/examples/qgis-js-example-api/vite.config.js",
    "content": "import { defineConfig } from \"vite\";\n\nexport default defineConfig({\n  preview: {\n    headers: {\n      \"Cross-Origin-Opener-Policy\": \"same-origin\",\n      \"Cross-Origin-Embedder-Policy\": \"require-corp\",\n    },\n  },\n});\n"
  },
  {
    "path": "docs/examples/qgis-js-example-ol/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <title>qgis-js-example-ol</title>\n    <link rel=\"icon\" href=\"data:,\" />\n    <link rel=\"stylesheet\" href=\"style.css\" />\n  </head>\n  <body>\n    <h2>🗺️ Minimal OpenLayers example</h2>\n    <div id=\"map\"></div>\n    <script type=\"module\" src=\"main.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "docs/examples/qgis-js-example-ol/main.js",
    "content": "import { qgis } from \"qgis-js\";\n\nimport { QgisCanvasDataSource } from \"@qgis-js/ol\";\n\nimport { Map, View } from \"ol\";\n\nimport ImageLayer from \"ol/layer/Image\";\nimport Projection from \"ol/proj/Projection.js\";\n\n// start the qgis-js runtime\nconst { api, fs } = await qgis({\n  prefix: \"/assets/wasm\",\n});\n\n// prepare the upload directory\nconst uploadDir = \"/upload\";\nfs.mkdir(uploadDir);\n\n// fetch demo project and upload it to the runtime\nconst source =\n  \"https://raw.githubusercontent.com/boardend/qgis-js-projects/main/demo/World%20GPKG\";\nconst files = [\"project.qgz\", \"world_map.gpkg\"];\nfor (const file of files) {\n  const url = `${source}/${file}`;\n  const response = await fetch(url);\n  const blob = await response.blob();\n  const buffer = await blob.arrayBuffer();\n  fs.writeFile(`${uploadDir}/${file}`, new Uint8Array(buffer));\n}\n\n// load the uploaded project\napi.loadProject(`${uploadDir}/${files[0]}`);\n\n// create the ol map with the projection from the project\nconst projection = new Projection({\n  code: api.srid(),\n  units: \"m\",\n});\n\nfunction createQgisLayer() {\n  return new ImageLayer({\n    source: new QgisCanvasDataSource(api, {\n      projection,\n    }),\n  });\n}\n\nconst map = new Map({\n  target: \"map\",\n  layers: [createQgisLayer()],\n  view: new View({\n    center: [0, 0],\n    zoom: 2,\n    projection,\n  }),\n});\n\n// create a dropdown with all map themes\nconst mapThemes = api.mapThemes();\nif (mapThemes.length > 0) {\n  const themeContainer = document.createElement(\"div\");\n  themeContainer.style.marginTop = \"1em\";\n  document.body.appendChild(themeContainer);\n\n  themeContainer.appendChild(document.createTextNode(\"Map theme: \"));\n\n  const select = document.createElement(\"select\");\n  select.addEventListener(\"change\", () => {\n    if (select.value) {\n      api.setMapTheme(select.value);\n      map.getLayers().clear();\n      map.addLayer(createQgisLayer());\n    }\n  });\n  themeContainer.appendChild(select);\n\n  const currentTheme = api.getMapTheme();\n\n  const option = document.createElement(\"option\");\n  option.value = \"\";\n  option.text = \"\";\n  if (!currentTheme) {\n    option.selected = true;\n  }\n  select.appendChild(option);\n\n  for (const theme of api.mapThemes()) {\n    const option = document.createElement(\"option\");\n    option.value = theme;\n    option.text = theme;\n    if (theme === currentTheme) {\n      option.selected = true;\n    }\n    select.appendChild(option);\n  }\n}\n"
  },
  {
    "path": "docs/examples/qgis-js-example-ol/package.json",
    "content": "{\n  \"name\": \"qgis-js-example-ol\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"start\": \"vite dev\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"ol\": \"^10.8.0\",\n    \"@qgis-js/ol\": \"4.0.0\",\n    \"qgis-js\": \"4.0.0\"\n  },\n  \"devDependencies\": {\n    \"vite\": \"^8.0.0\",\n    \"vite-plugin-static-copy\": \"^3.2.0\"\n  }\n}\n"
  },
  {
    "path": "docs/examples/qgis-js-example-ol/style.css",
    "content": "@import \"node_modules/ol/ol.css\";\n\nbody {\n  font-family: sans-serif;\n  margin: 1em;\n  padding: 1em;\n}\n#map {\n  width: 800px;\n  height: 600px;\n  border: 1px solid grey;\n}\n"
  },
  {
    "path": "docs/examples/qgis-js-example-ol/vite.config.js",
    "content": "import { defineConfig } from \"vite\";\n\nimport { viteStaticCopy } from \"vite-plugin-static-copy\";\n\nconst headers = {\n  \"Cross-Origin-Opener-Policy\": \"same-origin\",\n  \"Cross-Origin-Embedder-Policy\": \"require-corp\",\n};\n\nexport default defineConfig({\n  build: {\n    target: \"esnext\",\n  },\n  plugins: [\n    {\n      name: \"headers-after-static-copy\",\n      configureServer(server) {\n        server.middlewares.use((_req, res, next) => {\n          Object.entries(headers).forEach(([key, value]) => {\n            res.setHeader(key, value);\n          });\n          next();\n        });\n      },\n    },\n    viteStaticCopy({\n      silent: true,\n      targets: [\n        {\n          src: \"node_modules/qgis-js/dist/assets/wasm/**\",\n          dest: \"assets/wasm\",\n        },\n      ],\n    }),\n  ],\n  preview: {\n    headers,\n  },\n});\n"
  },
  {
    "path": "docs/performance.md",
    "content": "# Performance and Optimization\n\n## Preface\n\nIn Feb-June 2024, we will conduct a performance test to measure the performance of qgis-js and find ways to optimize it. This document outlines the general approach, lists the potential optimizations and tracks the status of those optimizations\n\nMore information\n\n- [Performance Comparison (between version 0.0.3 and 0.0.5)](https://github.com/boardend/qgis-js-performance)\n- Documentation\n  - [`./debugging.md`](./debugging.md)\n  - [`./profiling.md`](./profiling.md)\n\n## Deliverables\n\n- [x] [A performance measurement tool for qgis-js](../sites/performance/)\n  - [x] [A report on the performance gain](https://github.com/boardend/qgis-js-performance)\n- [x] [A final conclusion](#conclusion)\n\n## Conclusion\n\n**Approach**:\n\n- In order to optimize the performance of qgis-js, we have gathered a list of potential [optimizations](#optimizations) and implemented as many as possible in the given time frame\n  - The status of each optimization is tracked in the [list](#optimizations)\n  - Note that most of them have been implemented\n- To challenge the performance of qgis-js, the demo project \"AoS - Precipitation per balance basin\" with the worst observed performance so far has been selected to verify the optimizations\n  - It is used on its worst performing extent (whole of Switzerland) and used to render a full screen map (`1920x1080`)\n  - The project was measured in two versions:\n    - [`aos-baseline`](https://github.com/boardend/qgis-js-projects/tree/main/performance/aos-baseline): The original version of the project, as it is used on the qgis-js website\n    - [`aos-playground`](https://github.com/boardend/qgis-js-projects/tree/main/performance/aos-playground): A optimized version of the project\n      - Added/Recalculate indices for all layers\n      - Simplified geometries for all vector layers\n      - Played with the project settings to achieve potential performance improvements\n- A performance measurement tool has been implemented used to measure the performance of the application before (version `0.0.3`) and after the optimizations (version `0.0.5`) and to compare the performance of the `aos-baseline` and `aos-playground` project:\n  - The full results can be found in a seperate repository: [Performance Comparison (between version 0.0.3 and 0.0.5)](https://github.com/boardend/qgis-js-performance)\n\n> All changes in the scope of this project have been merged with [PR #38](https://github.com/qgis/qgis-js/pull/38)\n\n**Results**:\n\n- Rendering time could be reduced significantly between version `0.0.3` and `0.0.5`:\n  - Chrome: `41.60%`\n  - Firefox: `43.04%`\n- Chrome is performing better than Firefox\n  - Ratio rendering time Chrome/Firefox: `0.47`\n- Difference between `aos-baseline`/`aos-playground` project is negligible\n  - `100.38%`, `100.51%`, `93.66%`, `101.62%`\n\n> see [Performance Comparison (between version 0.0.3 and 0.0.5)](https://github.com/boardend/qgis-js-performance)\n\n> Compare the two versions in the browser:\n>\n> - \\>= `0.0.5`: https://qgis.github.io/qgis-js/\n> - `0.0.3`: https://boardend.github.io/qgis-js-baseline/\n\n**Further steps**:\n\n- Implement the not yet implemented [optimizations](#optimizations)\n- Add the performance measurement tool to the GitHub page\n- Automate the performance measurement tool to run on every commit/release\n- Further [profile](./profiling.md) the performance of the application in order to find bottlenecks\n- Compare the performance of the WebAssembly build against the native build of QGIS\n\n## Optimizations\n\n> 💡 **Legend**: Each potential optimization will be prioritized with one of the following labels:\n>\n> **Priority**:\n>\n> - 🟢 High priority: _Should definitively taken into account_\n> - 🟡 Mid priority: _Would be nice to investigate_\n> - 🔴 Low priority: _Probably out of scope for now_\n>\n> **Status**:\n>\n> - [x] Implemented\n> - [ ] Open\n\n### Toolchain/Dependencies\n\n- [x] 🟢 Update to latest emscripten version\n  - currently `3.1.29` is used (more than a year old)\n  - by anecdotal evidence, binaries compiled with the latest version are faster (latest LLVM)\n- [x] 🟢 Update to Qt version `6.6.1`\n  - see [wasm improvements since `6.5.2`](https://github.com/qt/qtreleasenotes/tree/dev/qt)\n  - building of Qt is needed anyway, in order to pass custom build flags (see below)\n- [x] 🟡 Update to vcpkg version `2024.02.14` (and update the packages with it)\n- [ ] 🔴 Update to latest QGIS version\n  - Currently `3.32.1` is used\n  - Latest would be `3.34.3`\n  - 💬 Created an issue to track this: https://github.com/qgis/qgis-js/issues/39\n\n### Build/Setup\n\n- [x] 🟢 Internalize Qt build\n  - This makes is possible to also apply optimizations also to the Qt build\n  - Check if the Qt build configuration can be optimized\n- [x] 🟢 Review and tweak the build flags/settings provided to emscripten\n  - `-flto` (Link Time Optimization)\n  - see https://emscripten.org/docs/optimizing/Optimizing-Code.html\n  - see https://emsettings.surma.technology/\n- [x] 🟢 Add [SIMD support](https://webassembly.org/features/)\n  - Make use of auto vectorization\n  - Add SIMD support for libraries (e.g. GDAL)\n  - see https://jeromewu.github.io/improving-performance-using-webassembly-simd-intrinsics/\n- [x] 🟢 Ensure [Bulk memory operations](https://webassembly.org/features/)\n  - Not sure if we already make use of this\n  - See `-mbulk-memory`\n  - 💬 Note that this is the default of newer Emscripten versions\n- [ ] 🟢 Check if new [WASMFS](https://emscripten.org/docs/api_reference/Filesystem-API.html#new-file-system-wasmfs) helps to improve FS performance\n  - 💬 This was not working well with the Qt CMake setup\n    - Needs more time to investigate\n- [x] 🟡 Check other potential [WebAssembly features](https://webassembly.org/features/)\n  - See if we can make use of any other of features generally available\n  - 💬 Didn't find anything that could benefit the current setup\n\n### Architecture/Runtime\n\n- **QGIS**\n  - [x] 🟢 Switch to `QgsMapRendererParallelJob`\n  - [x] 🟢 Check if auto geometry simplification is configured and used\n  - [x] 🟢 Check if `QgsMapRendererCache` can help to speed up redraws\n    - 💬 This doesn't help with the general rending performance. But could improove e.g. layer toggling. Not in use at the moment\n  - [x] 🟡 Check all `MapSettingsFlag`, `ProjectReadFlag`, etc. for potential tweaks\n  - [x] 🟡 Have a look at the QGIS Server implementation for potential tweaks\n  - **API**\n    - [x] 🟢 Check if JS promises can be returned directly from the API\n      - 💬 This is possible with Emscriptens [promise.h](https://github.com/emscripten-core/emscripten/blob/main/system/include/emscripten/promise.h). But won't help much with the general render performance\n    - [x] 🟡 Check if it is possible to make a zero-copy implementation of the render callback (e.g. [transferable](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects))\n      - 💬 This is not possible with the current architecture (But the copy takes less then 5ms)\n- **Qt**\n  - [ ] 🟢 Check performance impact of OutputImageFormat `Format_ARGB32`\n    - Is it faster to render in `Format_ARGB32_Premultiplied` and convert to `Format_ARGB32` afterwards?\n- **sqlite**\n  - [x] 🟢 Check if WAL/locking etc. can be disabled for rendering (read-only)\n    - 💬 See the flags in `qgis-js.cpp`\n\n### UI/UX\n\n- [x] **OpenLayers**\n  - [x] 🟢 Check with the OL devs, if the current layer implementations are well designed\n    - 💬 Checked with [Andreas Hocevar](https://github.com/ahocevar) and created this follow up issues:\n      - https://github.com/qgis/qgis-js/issues/40 , https://github.com/qgis/qgis-js/issues/41 , https://github.com/qgis/qgis-js/issues/42 , https://github.com/qgis/qgis-js/issues/16\n  - [x] 🟢 Cancellation of pending render jobs\n    - 💬 Implemented with the `QgisJobDataSource`\n  - [x] 🟢 Find a way to cache the results in \"canvas\" mode (like in XYZ mode)\n    - 💬 Not possible at the moment (only with tiling like here: https://openlayers.org/en/latest/examples/wms-custom-proj.html)\n  - [x] 🟡 Is it possible to display immediate results (progressive rendering), before the job finishes\n    - 💬 Implemented with the `QgisJobDataSource`\n- [ ] 🔴 **QML**\n  - Compare the performance of `QgsQuickMapCanvasMap` with the performance and UX of the current OL solution\n\n### QGIS Project\n\n- [x] 🟢 Optimize the project settings for fastest possible rendering\n  - 💬 This doesn't help much. See [`aos-baseline`](https://github.com/boardend/qgis-js-projects/tree/main/performance/aos-baseline) vs [`aos-playground`](https://github.com/boardend/qgis-js-projects/tree/main/performance/aos-playground) when running in local dev mode.\n- [x] 🟢 Ensure indices are created and used for all vector layers\n  - 💬 This was already in place for the checked vector and raster layers ([`aos-baseline`](https://github.com/boardend/qgis-js-projects/tree/main/performance/aos-baseline))\n- [ ] 🟡 Check if there are faster data formats than GeoPackage (e.g. FlatGeobuf)\n"
  },
  {
    "path": "docs/profiling.md",
    "content": "# Profiling\n\n## qgis-js Performance Measurement Tool\n\nThe [qgis-js Performance Measurement Tool](../sites/performance/) can be used to measure the performance of the qgis-js application in a reproducible way:\n\n```\ncd sites/performance\nnpm run dev\n```\n\n1. Boot the runtime\n2. Load a project\n3. Render a first dummy frame\n4. Start the performance test\n\n## Browsers\n\n### Chrome\n\nThe Performance tab of the Chrome DevTools can be used to profile the performance of the qgis-js application. See the [official documentation](https://developer.chrome.com/docs/devtools/evaluate-performance/) for more information.\n\n![Firefox Profiler](https://developer.chrome.com/static/docs/devtools/performance/image/the-results-the-profile-5d830d01508e2_2880.png)\n\n- 💡 In order to get useful results (e.g. function names in the `.wasm` module), the [build type](../README.md#build-types) has to be set to `Dev` or `Debug`\n  - ⚠️ Note that the `Debug` build type is significantly slower than the `Dev` build type\n\n### Firefox\n\nThe [Firefox Profiler⁩](https://profiler.firefox.com/) can be used to profile the performance of the qgis-js application.\n\n![Firefox Profiler](https://profiler.firefox.com/b45b29da558efa211628.jpg)\n\n- 💡 In order to get useful results (e.g. function names in the `.wasm` module), the [build type](../README.md#build-types) has to be set to `Dev` or `Debug`\n  - ⚠️ Note that the `Debug` build type is significantly slower than the `Dev` build type\n\n## Emscripten\n\n- Emscripten provies a [profiling guide](https://emscripten.org/docs/optimizing/Optimizing-Code.html#profiling) and some embeddable tools to profile the application:\n  - `--cpuprofiler`\n  - `--memoryprofiler`\n  - `--threadprofiler`\n\n## QGIS profiling\n\n- It would be nice to extend the qgis-js API in order to retrieve profiling information from the QGIS core\n  - ⚠️ This is not yet implemented\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"qgis-js-repo\",\n  \"private\": true,\n  \"version\": \"4.0.0\",\n  \"license\": \"GPL-2.0-or-later\",\n  \"homepage\": \"https://qgis.github.io/qgis-js/\",\n  \"repository\": \"github:qgis/qgis-js\",\n  \"bugs\": {\n    \"url\": \"https://github.com/qgis/qgis-js/issues\"\n  },\n  \"packageManager\": \"pnpm@10.32.1\",\n  \"engines\": {\n    \"node\": \"22.16.0\",\n    \"pnpm\": \"10.32.1\",\n    \"emsdk\": \"5.0.2\",\n    \"vcpkg\": \"2025.12.12\",\n    \"qt\": \"6.10.1\",\n    \"qgis\": \"4.0.0\"\n  },\n  \"type\": \"module\",\n  \"scripts\": {\n    \"postinstall\": \"./qgis-js.ts -v install\",\n    \"update\": \"pnpm update -r --ignore-scripts\",\n    \"clean\": \"./qgis-js.ts -v clean\",\n    \"compile\": \"pnpm run compile:dev\",\n    \"compile:dev\": \"./qgis-js.ts compile\",\n    \"compile:debug\": \"./qgis-js.ts compile -t Debug\",\n    \"compile:release\": \"./qgis-js.ts compile -t Release\",\n    \"build\": \"pnpm -r --filter=./packages/** run build\",\n    \"dev\": \"pnpm --filter @qgis-js/dev dev\",\n    \"dev:build\": \"pnpm --filter @qgis-js/dev build\",\n    \"dev:preview\": \"npm run dev:build && pnpm --filter @qgis-js/dev preview\",\n    \"site\": \"vite dev\",\n    \"deploy\": \"pnpm -r --filter=./sites/** run deploy\",\n    \"publish\": \"pnpm publish -r --filter=./packages/qgis-js** --access=public\",\n    \"lint\": \"npm run lint:prettier && npm run lint:clang-format\",\n    \"lint:prettier\": \"npx prettier . --write\",\n    \"lint:pretty-quick\": \"pretty-quick --staged\",\n    \"lint:clang-format\": \"clang-format -i \\\"--glob=src/**/*.{cpp,hpp}\\\"\"\n  },\n  \"devDependencies\": {\n    \"pnpm\": \"10.32.1\",\n    \"vite\": \"8.0.0\",\n    \"vite-node\": \"6.0.0\",\n    \"zx\": \"8.8.5\",\n    \"@rushstack/ts-command-line\": \"5.3.3\",\n    \"ts-markdown\": \"1.3.0\",\n    \"@microsoft/api-extractor\": \"7.57.7\",\n    \"@microsoft/api-documenter\": \"7.29.7\",\n    \"prettier\": \"3.8.1\",\n    \"pretty-quick\": \"4.2.2\",\n    \"clang-format\": \"1.8.0\"\n  }\n}\n"
  },
  {
    "path": "packages/qgis-js/README.md",
    "content": "# qgis-js\n\n**QGIS core ported to WebAssembly to run it on the web platform**\n\nVersion: `4.0.0` (based on QGIS 4.0.0)\n\n[qgis-js Repository](https://github.com/qgis/qgis-js) | [qgis-js Website](https://qgis.github.io/qgis-js) | [\"`qgis-js`\" package source](https://github.com/qgis/qgis-js/tree/main/packages/qgis-js)\n\n[![qgis-js on npm](https://img.shields.io/npm/v/qgis-js)](https://www.npmjs.com/package/qgis-js)\n\n> ⚠️🧪 **Work in progress**! Currently this project is in public beta\n\n## Description\n\nQGIS core compiled to WebAssembly to run it on the web platform. This package provides the WebAssembly module and JavaScript/TypeScript API to load the runtime and interact with QGIS.\n\nSee the [qgis-js repository](https://github.com/qgis/qgis-js) for more information about the project.\n\n## Installation\n\n```bash\nnpm install -S qgis-js\n```\n\n## Usage\n\n```js\nimport { qgis } from \"qgis-js\";\n\nconst { api } = await qgis();\n\nconst rect = new api.QgsRectangle(1, 2, 3, 4);\nrect.scale(5);\nconst center = rect.center();\nconsole.log(center.x, center.y);\n```\n\n> 💡 Have a look at the [Integration packages](#integration-packages) to load QGIS projects and display them on a map\n\n> ⚠️ It must be ensured that...\n>\n> - the clients meets the [compatibility requirements](https://github.com/qgis/qgis-js/blob/main/docs/compatibility.md)\n> - that the webserver is configured to [allow cross-origin requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)\n> - that if you are using a bundler, it is [configured to load the qgis-js assets](https://github.com/qgis/qgis-js/blob/main/docs/bundling.md)\n\n## Integration packages\n\n| Package                                                  | Description                                                           | npm                                                                                                                   |\n| -------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |\n| **[@qgis-js/ol](./packages/qgis-js-ol/README.md)**       | [OpenLayers](https://openlayers.org/) sources to display qgis-js maps | [![@qgis-js/ol on npm](https://img.shields.io/npm/v/@qgis-js/ol)](https://www.npmjs.com/package/@qgis-js/ol)          |\n| **[@qgis-js/utils](./packages/qgis-js-utils/README.md)** | Utilities to integrate qgis-js into web applications                  | [![@qgis-js/utils on npm](https://img.shields.io/npm/v/@qgis-js/utils)](https://www.npmjs.com/package/@qgis-js/utils) |\n\n## WebAssembly module\n\n### Size\n\n<!--NOTE: this can be generated with \"./qgis-js.ts size -o markdown\"-->\n\nThe size of the package is **`77.06 MB`** (uncompressed) or `18.74 MB` Brotli compressed (76% space saving) / `22.08 MB` Gzip compressed (71% space saving)\n\nIt consists of the following files:\n\n| File name                  | Size (uncompressed) | Size (Brotli compressed)      | Size (Gzip compressed)        |\n| -------------------------- | ------------------- | ----------------------------- | ----------------------------- |\n| `qgis.js`                  | `5.19 kB`           | `1.87 kB` (64% space saving)  | `1.97 kB` (62% space saving)  |\n| `assets/wasm/qgis-js.js`   | `276.92 kB`         | `61.28 kB` (78% space saving) | `65.16 kB` (76% space saving) |\n| `assets/wasm/qgis-js.data` | `13.4 MB`           | `2.01 MB` (85% space saving)  | `2.62 MB` (80% space saving)  |\n| `assets/wasm/qgis-js.wasm` | `63.39 MB`          | `16.67 MB` (74% space saving) | `19.4 MB` (69% space saving)  |\n\n### Libraries\n\n<!--NOTE: this can be generated with \"./qgis-js.ts libs -o markdown\"-->\n\n| Library                                                                                                                                                                                                                                                                                                                                                 | License                      | Links                                                                                                                       |\n| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- |\n| **abseil** (20260107.1)<br /><div style=\"max-width:30em\">_Abseil is an open-source collection of C++ library code designed to augment the C++ standard library. The Abseil library code is collected from Google's own C++ code base, has been extensively tested and used in production, and is the same code we depend on in our daily coding lives._ | Apache-2.0                   | [Website](https://github.com/abseil/abseil-cpp) - [Source code](https://github.com/abseil/abseil-cpp)                       |\n| **double-conversion** (3.4.0)<br /><div style=\"max-width:30em\">_Efficient binary-decimal and decimal-binary conversion routines for IEEE doubles._                                                                                                                                                                                                      |                              | [Website](https://github.com/google/double-conversion) - [Source code](https://github.com/google/double-conversion)         |\n| **egl-registry** (2025-05-27)<br /><div style=\"max-width:30em\">_EGL API and Extension Registry_                                                                                                                                                                                                                                                         |                              | [Website](https://github.com/KhronosGroup/EGL-Registry) - [Source code](https://github.com/KhronosGroup/EGL-Registry)       |\n| **exiv2** (0.28.8)<br /><div style=\"max-width:30em\">_Image metadata library and tools_                                                                                                                                                                                                                                                                  | GPL-2.0-or-later             | [Website](https://exiv2.org) - [Source code](https://github.com/Exiv2/exiv2)                                                |\n| **expat** (2.7.4)<br /><div style=\"max-width:30em\">_XML parser library written in C_                                                                                                                                                                                                                                                                    | MIT                          | [Website](https://github.com/libexpat/libexpat) - [Source code](https://github.com/libexpat/libexpat)                       |\n| **freetype** (2.13.3)<br /><div style=\"max-width:30em\">_A library to render fonts._                                                                                                                                                                                                                                                                     | FTL OR GPL-2.0-or-later      | [Website](https://www.freetype.org/) - [Source code](https://gitlab.freedesktop.org/freetype/freetype)                      |\n| **gdal** (3.12.2)<br /><div style=\"max-width:30em\">_The Geographic Data Abstraction Library for reading and writing geospatial raster and vector data_                                                                                                                                                                                                  |                              | [Website](https://gdal.org) - [Source code](https://github.com/OSGeo/gdal)                                                  |\n| **geos** (3.14.1)<br /><div style=\"max-width:30em\">_Geometry Engine Open Source_                                                                                                                                                                                                                                                                        | LGPL-2.1-only                | [Website](https://libgeos.org/)                                                                                             |\n| **inih** (62)<br /><div style=\"max-width:30em\">_Simple .INI file parser_                                                                                                                                                                                                                                                                                | BSD-3-Clause                 | [Website](https://github.com/benhoyt/inih) - [Source code](https://github.com/benhoyt/inih)                                 |\n| **json-c** (0.18-20240915)<br /><div style=\"max-width:30em\">_A JSON implementation in C_                                                                                                                                                                                                                                                                | MIT                          | [Website](https://github.com/json-c/json-c) - [Source code](https://github.com/json-c/json-c)                               |\n| **libb2** (0.98.1)<br /><div style=\"max-width:30em\">_C library providing BLAKE2b, BLAKE2s, BLAKE2bp, BLAKE2sp_                                                                                                                                                                                                                                          |                              | [Website](https://github.com/BLAKE2/libb2) - [Source code](https://github.com/BLAKE2/libb2)                                 |\n| **libgeotiff** (1.7.4)<br /><div style=\"max-width:30em\">_Libgeotiff is an open source library on top of libtiff for reading and writing GeoTIFF information tags._                                                                                                                                                                                      | MIT                          | [Website](https://github.com/OSGeo/libgeotiff)                                                                              |\n| **libiconv** (1.18)<br /><div style=\"max-width:30em\">_iconv() text conversion._                                                                                                                                                                                                                                                                         |                              | [Website](https://www.gnu.org/software/libiconv/)                                                                           |\n| **libjpeg-turbo** (3.1.3)<br /><div style=\"max-width:30em\">_libjpeg-turbo is a JPEG image codec that uses SIMD instructions (MMX, SSE2, NEON, AltiVec) to accelerate baseline JPEG compression and decompression on x86, x86-64, ARM, and PowerPC systems._                                                                                             | BSD-3-Clause                 | [Website](https://github.com/libjpeg-turbo/libjpeg-turbo)                                                                   |\n| **liblzma** (5.8.2)<br /><div style=\"max-width:30em\">_Compression library with an API similar to that of zlib._                                                                                                                                                                                                                                         |                              | [Website](https://tukaani.org/xz/)                                                                                          |\n| **libpng** (1.6.55)<br /><div style=\"max-width:30em\">_libpng is a library implementing an interface for reading and writing PNG (Portable Network Graphics) format files_                                                                                                                                                                               | libpng-2.0                   | [Website](https://github.com/pnggroup/libpng)                                                                               |\n| **libspatialindex** (2.0.0)<br /><div style=\"max-width:30em\">_C++ implementation of R\\*-tree, an MVR-tree and a TPR-tree with C API._                                                                                                                                                                                                                   | MIT                          | [Website](http://libspatialindex.github.com)                                                                                |\n| **libzip** (1.11.4)<br /><div style=\"max-width:30em\">_A C library for reading, creating, and modifying zip archives._                                                                                                                                                                                                                                   | BSD-3-Clause                 | [Website](https://github.com/nih-at/libzip)                                                                                 |\n| **md4c** (0.5.2)<br /><div style=\"max-width:30em\">_MD4C is a C library providing a Markdown parser._                                                                                                                                                                                                                                                    | MIT                          | [Website](https://github.com/mity/md4c) - [Source code](https://github.com/mity/md4c)                                       |\n| **nlohmann-json** (3.12.0)<br /><div style=\"max-width:30em\">_JSON for Modern C++_                                                                                                                                                                                                                                                                       | MIT                          | [Website](https://github.com/nlohmann/json) - [Source code](https://github.com/nlohmann/json)                               |\n| **opengl-registry** (2026-01-26)<br /><div style=\"max-width:30em\">_OpenGL, OpenGL ES, and OpenGL ES-SC API and Extension Registry_                                                                                                                                                                                                                      |                              | [Website](https://github.com/KhronosGroup/OpenGL-Registry) - [Source code](https://github.com/KhronosGroup/OpenGL-Registry) |\n| **opengl** (2022-12-04)<br /><div style=\"max-width:30em\">_Open Graphics Library (OpenGL)[3][4][5] is a cross-language, cross-platform application programming interface (API) for rendering 2D and 3D vector graphics._                                                                                                                                 |                              |                                                                                                                             |\n| **pcre2** (10.47)<br /><div style=\"max-width:30em\">_Regular Expression pattern matching using the same syntax and semantics as Perl 5._                                                                                                                                                                                                                 | BSD-3-Clause                 | [Website](https://github.com/PCRE2Project/pcre2)                                                                            |\n| **proj** (9.7.1)<br /><div style=\"max-width:30em\">_PROJ library for cartographic projections_                                                                                                                                                                                                                                                           | MIT                          | [Website](https://proj.org/) - [Source code](https://github.com/OSGeo/PROJ)                                                 |\n| **protobuf** (6.33.4)<br /><div style=\"max-width:30em\">_Google's language-neutral, platform-neutral, extensible mechanism for serializing structured data._                                                                                                                                                                                             | BSD-3-Clause                 | [Website](https://github.com/protocolbuffers/protobuf) - [Source code](https://github.com/protocolbuffers/protobuf)         |\n| **qgis** (4.0.0)<br /><div style=\"max-width:30em\">_QGIS is a free, open source, cross platform (lin/win/mac) geographical information system (GIS)_                                                                                                                                                                                                     | GPL-2.0                      | [Website](https://www.qgis.org/) - [Source code](https://github.com/qgis/QGIS)                                              |\n| **qt5compat** (6.10.1)<br /><div style=\"max-width:30em\">_The Qt 5 Core Compat module contains the Qt 5 Core APIs that were removed in Qt 6. The module facilitates the transition to Qt 6._                                                                                                                                                             |                              | [Website](https://www.qt.io/)                                                                                               |\n| **qtbase** (6.10.1)<br /><div style=\"max-width:30em\">_Qt Base (Core, Gui, Widgets, Network, ...)_                                                                                                                                                                                                                                                       |                              | [Website](https://www.qt.io/)                                                                                               |\n| **qtkeychain** (0.14.3)<br /><div style=\"max-width:30em\">_(Unaffiliated with Qt) Platform-independent Qt6 API for storing passwords securely_                                                                                                                                                                                                           | BSD-3-Clause                 | [Website](https://github.com/frankosterfeld/qtkeychain) - [Source code](https://github.com/frankosterfeld/qtkeychain)       |\n| **qtmultimedia** (6.10.1)<br /><div style=\"max-width:30em\">_Qt Multimedia is an add-on module that provides a rich set of QML types and C++ classes to handle multimedia content._                                                                                                                                                                      |                              | [Website](https://www.qt.io/)                                                                                               |\n| **qtshadertools** (6.10.1)<br /><div style=\"max-width:30em\">_The Qt Shader Tools module is designed to provide a set of tools and utilities to work with graphics shaders._                                                                                                                                                                             |                              | [Website](https://www.qt.io/)                                                                                               |\n| **qtsvg** (6.10.1)<br /><div style=\"max-width:30em\">_Qt SVG provides classes for rendering and displaying SVG drawings in widgets and on other paint devices._                                                                                                                                                                                          |                              | [Website](https://www.qt.io/)                                                                                               |\n| **qttools** (6.10.1)<br /><div style=\"max-width:30em\">_A collection of tools and utilities that come with the Qt framework to assist developers in the creation, management, and deployment of Qt applications._                                                                                                                                        |                              | [Website](https://www.qt.io/)                                                                                               |\n| **sqlite3** (3.51.2)<br /><div style=\"max-width:30em\">_SQLite is a software library that implements a self-contained, serverless, zero-configuration, transactional SQL database engine._                                                                                                                                                               | blessing                     | [Website](https://sqlite.org/)                                                                                              |\n| **tiff** (4.7.1)<br /><div style=\"max-width:30em\">_A library that supports the manipulation of TIFF image files_                                                                                                                                                                                                                                        | libtiff                      | [Website](https://libtiff.gitlab.io/libtiff/) - [Source code](https://gitlab.com/libtiff/libtiff)                           |\n| **utf8-range** (6.33.4)<br /><div style=\"max-width:30em\">_Fast UTF-8 validation with Range algorithm (NEON+SSE4+AVX2)_                                                                                                                                                                                                                                  | MIT                          | [Website](https://github.com/protocolbuffers/protobuf) - [Source code](https://github.com/protocolbuffers/protobuf)         |\n| **zlib** (1.3.1)<br /><div style=\"max-width:30em\">_A compression library_                                                                                                                                                                                                                                                                               | Zlib                         | [Website](https://www.zlib.net/) - [Source code](https://github.com/madler/zlib)                                            |\n| **zstd** (1.5.7)<br /><div style=\"max-width:30em\">_Zstandard - Fast real-time compression algorithm_                                                                                                                                                                                                                                                    | BSD-3-Clause OR GPL-2.0-only | [Website](https://facebook.github.io/zstd/) - [Source code](https://github.com/facebook/zstd)                               |\n\n## Versioning\n\nThis package uses [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/qgis/qgis-js/tags).\n\n## License\n\n[GNU General Public License v2.0](https://github.com/qgis/qgis-js/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/qgis-js/package.json",
    "content": "{\n  \"name\": \"qgis-js\",\n  \"version\": \"4.0.0\",\n  \"description\": \"QGIS core ported to WebAssembly to run it on the web platform\",\n  \"license\": \"GPL-2.0-or-later\",\n  \"homepage\": \"https://qgis.github.io/qgis-js/\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/qgis/qgis-js\",\n    \"directory\": \"packages/qgis-js\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/qgis/qgis-js/issues\"\n  },\n  \"type\": \"module\",\n  \"main\": \"dist/qgis.js\",\n  \"types\": \"dist/qgis.d.ts\",\n  \"files\": [\n    \"dist/**/*\"\n  ],\n  \"scripts\": {\n    \"build\": \"vite build\"\n  },\n  \"dependencies\": {\n    \"@types/emscripten\": \"1.41.5\",\n    \"p-limit\": \"7.3.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.19.15\",\n    \"typescript\": \"5.8.2\",\n    \"vite\": \"8.0.0\",\n    \"vite-plugin-dts\": \"4.5.4\"\n  },\n  \"keywords\": [\n    \"qgis\",\n    \"qgisjs\",\n    \"qgis-js\",\n    \"qgiswasm\",\n    \"qgis-wasm\",\n    \"webassembly\",\n    \"wasm\",\n    \"geo\",\n    \"geospatial\"\n  ]\n}\n"
  },
  {
    "path": "packages/qgis-js/src/QgisApiAdapter.ts",
    "content": "import {\n  InternalQgisApi,\n  QgisApi,\n  QgisApiAdapter,\n} from \"../../../src/api/QgisApi\";\nimport { QgsRectangle } from \"../../../src/api/QgisModel\";\n\nimport { threadPoolSize } from \"./runtime\";\n\nimport pLimit from \"p-limit\";\nimport type { LimitFunction } from \"p-limit\";\n\nexport class QgisApiAdapterImplementation implements QgisApiAdapter {\n  private readonly _api: InternalQgisApi;\n  private readonly _threadPoolSize: number;\n  private readonly _limit: LimitFunction;\n\n  constructor(api: InternalQgisApi) {\n    this._api = api;\n    this._threadPoolSize = threadPoolSize();\n    this._limit = pLimit(this._threadPoolSize);\n  }\n\n  protected runLimited<T>(fn: () => Promise<T>): Promise<T> {\n    return this._limit(fn);\n  }\n\n  renderImage(\n    srid: string,\n    extent: QgsRectangle,\n    width: number,\n    height: number,\n    pixelRatio: number = window?.devicePixelRatio || 1,\n    layerIds?: string[],\n  ): Promise<ImageData> {\n    return this.runLimited(() => {\n      return new Promise((resolve) => {\n        this._api.renderImage(\n          srid,\n          extent,\n          width,\n          height,\n          pixelRatio,\n          (tileData) => {\n            const data = new Uint8ClampedArray(\n              tileData,\n            ) as Uint8ClampedArray<ArrayBuffer>;\n            const imageData = new ImageData(data, width, height);\n            resolve(imageData);\n          },\n          layerIds,\n        );\n      });\n    });\n  }\n  renderXYZTile(\n    x: number,\n    y: number,\n    z: number,\n    tileSize: number = 256,\n    pixelRatio: number = window?.devicePixelRatio || 1,\n    extentBuffer: number = 0,\n    layerIds?: string[],\n  ): Promise<ImageData> {\n    return this.runLimited(() => {\n      return new Promise((resolve) => {\n        this._api.renderXYZTile(\n          x,\n          y,\n          z,\n          tileSize,\n          pixelRatio,\n          extentBuffer,\n          (tileData) => {\n            const data = new Uint8ClampedArray(\n              tileData,\n            ) as Uint8ClampedArray<ArrayBuffer>;\n            const imageData = new ImageData(data, tileSize, tileSize);\n            resolve(imageData);\n          },\n          layerIds,\n        );\n      });\n    });\n  }\n\n  mapThemes(): readonly string[] {\n    const mapLayersRaw = this._api.mapThemes();\n    const result = new Array<string>(mapLayersRaw.size());\n    for (let i = 0; i < mapLayersRaw.size(); i++) {\n      result[i] = mapLayersRaw.get(i);\n    }\n    return result;\n  }\n}\n\nexport function getQgisApiProxy(api: InternalQgisApi): QgisApi {\n  const adapter = new QgisApiAdapterImplementation(api);\n\n  return new Proxy<QgisApi>(\n    // @ts-ignore\n    {},\n    {\n      has(_target, property) {\n        return property in adapter || property in api;\n      },\n      get(_target, property) {\n        if (property in adapter) {\n          // @ts-ignore\n          return adapter[property];\n        } else if (property in api) {\n          // @ts-ignore\n          return api[property];\n        }\n      },\n    },\n  );\n}\n"
  },
  {
    "path": "packages/qgis-js/src/emscripten.ts",
    "content": "/// <reference types=\"emscripten\" />\n\n/**\n * Extension of a EmscriptenModule that adds additional properties\n */\nexport interface EmscriptenRuntimeModule extends EmscriptenModule {\n  [x: string]: any;\n}\n\n/**\n * Emscripten file system\n *\n * {@link https://emscripten.org/docs/api_reference/Filesystem-API.html}\n */\nexport type EmscriptenFS = typeof FS;\n"
  },
  {
    "path": "packages/qgis-js/src/index.ts",
    "content": "/**\n * The version of qgis-js.\n */\nexport const QGIS_JS_VERSION: string =\n  //@ts-ignore (will be defined by vite)\n  __QGIS_JS_VERSION;\n\nexport type {\n  QgisApi,\n  QgisApiAdapter,\n  CommonQgisApi,\n  InternalQgisApi,\n  LayerDefinitionResult,\n} from \"../../../src/api/QgisApi\";\n\nexport type { EmscriptenFS } from \"./emscripten\";\n\nexport type {\n  QgsPointXY,\n  QgsRectangle,\n  QgsLayerTreeModelLegendNode,\n  QgsLayerTreeNode,\n  QgsLayerTreeGroup,\n  QgsLayerTreeLayer,\n  QgsMapLayer,\n  QgsVectorLayer,\n} from \"../../../src/api/QgisModel\";\n\nexport { LayerType, NodeType } from \"../../../src/api/QgisModel\";\n\nexport { qgis } from \"./loader\";\n"
  },
  {
    "path": "packages/qgis-js/src/loader.ts",
    "content": "import { getQgisApiProxy } from \"./QgisApiAdapter\";\n\nimport { threadPoolSize } from \"./runtime\";\nimport type {\n  QgisRuntime,\n  QgisRuntimeConfig,\n  QgisRuntimeModule,\n} from \"./runtime\";\n\nimport type { EmscriptenRuntimeModule } from \"./emscripten\";\n\n/**\n * Emscripten module configuration\n */\ninterface QtAppConfig {}\n\n/**\n * Interface for a Emscripten module that creates a Qt app instance.\n */\ninterface QtRuntimeFactory {\n  createQtAppInstance(config: QtAppConfig): Promise<QgisRuntimeModule>;\n}\n\n/**\n * Loads the QtRuntimeFactory Emscripten module with the given prefix.\n *\n * @param mainScriptPath - The import path of the main script\n * @returns A promise that resolves with the QtRuntimeFactory module.\n */\nfunction loadModule(mainScriptPath: string): Promise<QtRuntimeFactory> {\n  return new Promise(async (resolve, reject) => {\n    try {\n      // hack to import es module without vite knowing about it\n      const createQtAppInstance = (\n        await new Function(`return import(\"${mainScriptPath}\")`)()\n      ).default;\n      resolve({\n        createQtAppInstance,\n      });\n    } catch (error) {\n      reject(error);\n    }\n  });\n}\n\n/**\n * Load and initialize a new qgis-js runtime.\n *\n * @param config The {@link QgisRuntimeConfig} that will be taken into account during loading and initialization.\n * @returns A promise that resolves to a {@link QgisRuntime}.\n */\nexport async function qgis(\n  config: QgisRuntimeConfig = {},\n): Promise<QgisRuntime> {\n  return new Promise(async (resolve, reject) => {\n    let prefix: string | undefined = undefined;\n    if (config.prefix) {\n      prefix = config.prefix;\n    } else {\n      const url = import.meta.url;\n      if (/.*\\/src\\/loader\\.[ts|js]\\?*[^/]*$/.test(url)) {\n        console.warn(\n          [\n            `qgis-js loader is running in development mode and no \"prefix\" seems to be configured.`,\n            ` - Consider adding the QgisRuntimePlugin when bundling with Vite.`,\n            ` - For more information see: https://github.com/qgis/qgis-js/blob/main/docs/bundling.md`,\n          ].join(\"\\n\"),\n        );\n        if (typeof window !== \"undefined\") {\n          prefix = new URL(\"assets/wasm\", window.location.href).pathname;\n        }\n      } else {\n        prefix = new URL(/* @vite-ignore */ \"assets/wasm\", import.meta.url)\n          .href;\n      }\n    }\n\n    if (!prefix) {\n      prefix = \"assets/wasm\";\n    } else {\n      prefix = prefix.replace(/\\/$/, \"\"); // ensure no trailing slash\n    }\n\n    let qtRuntimeFactory: QtRuntimeFactory | undefined = undefined;\n    try {\n      const mainScriptPath = `${prefix}/qgis-js.js`;\n      qtRuntimeFactory = await loadModule(mainScriptPath);\n    } catch (error) {\n      reject(\n        new Error(`Unable to load the qgis-js.js script`, { cause: error }),\n      );\n      return;\n    }\n\n    const { createQtAppInstance } = qtRuntimeFactory!;\n\n    let canvas: HTMLDivElement | undefined = undefined;\n    if (typeof document !== \"undefined\") {\n      canvas = document?.querySelector(\"#screen\") as HTMLDivElement;\n    }\n\n    const runtimePromise = createQtAppInstance({\n      locateFile: (path: string) => `${prefix}/` + path,\n      preRun: [\n        function (module: any) {\n          module.qtContainerElements = canvas ? [canvas] : [];\n          module.qtFontDpi = 96;\n          module.qgisJsMaxThreads = threadPoolSize();\n        },\n      ],\n      postRun: [\n        async function () {\n          const runtime = await runtimePromise;\n          resolve({\n            api: getQgisApiProxy(runtime),\n            module: runtime as EmscriptenRuntimeModule,\n            fs: runtime.FS,\n          });\n        },\n      ],\n      ...(config.onStatus ? { setStatus: config.onStatus } : {}),\n    });\n  });\n}\n"
  },
  {
    "path": "packages/qgis-js/src/runtime.ts",
    "content": "import { EmscriptenRuntimeModule, EmscriptenFS } from \"./emscripten\";\n\nimport { QgisApi, InternalQgisApi } from \"../../../src/api/QgisApi\";\n\n/**\n * Qt emscripten runtime module that exposes the QgisInternalApi\n */\nexport interface QgisRuntimeModule\n  extends EmscriptenRuntimeModule, InternalQgisApi {}\n\n/**\n * Boot configuration options for the QGIS runtime.\n */\nexport interface QgisRuntimeConfig {\n  /**\n   * The prefix to use for the {@link EmscriptenRuntimeModule} path.\n   */\n  prefix?: string;\n  /**\n   * A callback function that will be called when the runtime status changes.\n   */\n  onStatus?: (status: string) => void;\n}\n\n/**\n * Wraps the {@link EmscriptenRuntimeModule} and exposes the {@link QgisApi} and {@link EmscriptenFS}\n */\nexport interface QgisRuntime {\n  api: QgisApi;\n  module: EmscriptenRuntimeModule;\n  fs: EmscriptenFS;\n}\n\n/**\n * Returns the thread pool size based on the hardware concurrency of the user's device.\n * The thread pool size is capped between a minium of 4 and a maximum 16 threads.\n *\n * @privateRemarks This needs to be in sync with PTHREAD_POOL_SIZE in CMakeLists.txt\n *\n * @returns The thread pool size of the qgis-js runtime\n */\nexport function threadPoolSize() {\n  const MINIMAL_THREAD_POOL_SIZE = 4;\n  const MAXIMAL_THREAD_POOL_SIZE = 16;\n  return Math.min(\n    Math.max(\n      navigator?.hardwareConcurrency || MINIMAL_THREAD_POOL_SIZE,\n      MINIMAL_THREAD_POOL_SIZE,\n    ),\n    MAXIMAL_THREAD_POOL_SIZE,\n  );\n}\n"
  },
  {
    "path": "packages/qgis-js/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/qgis-js/vite.config.ts",
    "content": "import { resolve } from \"path\";\nimport { defineConfig } from \"vite\";\n\nimport QgisRuntimePlugin from \"../../build/vite/QgisRuntimePlugin\";\n\nimport dts from \"vite-plugin-dts\";\n\nimport packageJson from \"./package.json\";\n\nexport default defineConfig({\n  define: {\n    __QGIS_JS_VERSION: JSON.stringify(packageJson.version),\n  },\n  plugins: [\n    QgisRuntimePlugin({\n      name: \"qgis-js\",\n      outputDir: \"build/wasm\",\n    }),\n    dts({\n      copyDtsFiles: true,\n\n      staticImport: true,\n      // insertTypesEntry: true,\n      compilerOptions: {\n        declarationMap: true,\n      },\n\n      rollupTypes: true,\n      entryRoot: \"src\",\n      rollupConfig: {\n        docModel: {\n          enabled: true,\n          apiJsonFilePath: resolve(__dirname, \"etc/qgis-js.api.json\"),\n        },\n      },\n      async afterBuild() {\n        // remove empty export statement\n        const fs = await import(\"fs\");\n        const path = await import(\"path\");\n        const dtsFile = path.join(__dirname, \"dist\", \"qgis.d.ts\");\n        let content = fs.readFileSync(dtsFile, \"utf-8\");\n        content = content.replace(\"export { }\", \"\");\n\n        // format file with prettier\n        const prettier = await import(\"prettier\");\n        const prettierConfig = await prettier.resolveConfig(\n          path.join(__dirname, \"../..\", \".prettierrc.json\"),\n        );\n\n        content = await prettier.format(content, {\n          ...prettierConfig,\n          parser: \"typescript\",\n        });\n\n        fs.writeFileSync(dtsFile, content);\n        return;\n      },\n    }),\n  ],\n  build: {\n    lib: {\n      entry: [resolve(__dirname, \"src/index.ts\")],\n      name: \"qgis-js\",\n      formats: [\"es\"],\n      fileName: \"qgis\",\n    },\n  },\n});\n"
  },
  {
    "path": "packages/qgis-js-ol/README.md",
    "content": "# @qgis-js/ol\n\n**OpenLayers sources for [qgis-js](https://github.com/qgis/qgis-js)**\n\n[qgis-js Repository](https://github.com/qgis/qgis-js) | [qgis-js Website](https://qgis.github.io/qgis-js) | [\"`@qgis-js/ol`\" package source](https://github.com/qgis/qgis-js/tree/main/packages/qgis-js-ol)\n\n[![@qgis-js/ol on npm](https://img.shields.io/npm/v/@qgis-js/ol)](https://www.npmjs.com/package/@qgis-js/ol)\n\n> ⚠️🧪 **Work in progress**! Currently this project is in public beta\n\n## Prerequisites\n\n- This package requires **[OpenLayers](https://openlayers.org) `>=8`** to be installed as a peer dependency\n\n- The [qgis-js](https://www.npmjs.com/package/@qgis-js/ol) package is also required as a direct dependency of this package\n  - An instance of the qgis-js runtime has to be created at runtime and its API must be passed to the OpenLayers source constructor\n\n## Installation\n\n```bash\nnpm install -S @qgis-js/ol\n```\n\n## Usage\n\n### QgisCanvasDataSource\n\n[OpenLayers](https://openlayers.org) source for rendering a single tile with the size and pixel ratio of the ol map canvas. This is useful for rendering in the projection of the QGIS project, both in the OpenLayers view and in the qgis-js runtime.\n\n> See [QgisCanvasDataSource.ts](https://github.com/qgis/qgis-js/blob/main/packages/qgis-js-ol/src/QgisCanvasDataSource.ts) for the implementation.\n\n### QgisXYZDataSource\n\n[OpenLayers](https://openlayers.org) source to render a QGIS project in the common Web Mercator projection (EPSG:3857) addressed with the xyz tile scheme. This makes it convenient to mix the QGIS layer with other layer sources provided by OpenLayers.\n\n> See [QgisXYZDataSource.ts](https://github.com/qgis/qgis-js/blob/main/packages/qgis-js-ol/src/QgisXYZDataSource.ts) for the implementation.\n\n## Versioning\n\nThis package uses [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/qgis/qgis-js/tags).\n\n## License\n\n[GNU General Public License v2.0](https://github.com/qgis/qgis-js/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/qgis-js-ol/package.json",
    "content": "{\n  \"name\": \"@qgis-js/ol\",\n  \"version\": \"4.0.0\",\n  \"description\": \"OpenLayers sources for qgis-js\",\n  \"license\": \"GPL-2.0-or-later\",\n  \"homepage\": \"https://qgis.github.io/qgis-js/\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/qgis/qgis-js\",\n    \"directory\": \"packages/qgis-js-ol\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/qgis/qgis-js/issues\"\n  },\n  \"type\": \"module\",\n  \"main\": \"dist/qgis-js-ol.js\",\n  \"types\": \"dist/qgis-js-ol.d.ts\",\n  \"files\": [\n    \"dist/**/*\"\n  ],\n  \"scripts\": {\n    \"build\": \"vite build\"\n  },\n  \"dependencies\": {\n    \"qgis-js\": \"workspace:*\"\n  },\n  \"peerDependencies\": {\n    \"ol\": \"^10.8.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.19.15\",\n    \"typescript\": \"5.8.2\",\n    \"vite\": \"8.0.0\",\n    \"vite-plugin-dts\": \"4.5.4\"\n  }\n}\n"
  },
  {
    "path": "packages/qgis-js-ol/src/QgisCanvasDataSource.ts",
    "content": "import type { QgisApi } from \"qgis-js\";\n\nimport ImageSource, { Options } from \"ol/source/Image\";\nimport { getWidth, getHeight } from \"ol/extent\";\n\nexport interface QgisCanvasDataSourceOptions extends Options {\n  layerIds?: string[];\n  renderFunction?: QgisCanvasRenderFunction;\n}\n\nexport type QgisCanvasRenderFunction = (\n  api: QgisApi,\n  srid: string,\n  xMin: number,\n  yMin: number,\n  xMax: number,\n  yMax: number,\n  width: number,\n  height: number,\n  pixelRatio: number,\n  layerIds?: string[],\n) => Promise<ImageData>;\n\nexport class QgisCanvasDataSource extends ImageSource {\n  protected api: QgisApi;\n\n  protected static DEFAULT_RENDERFUNCTION: QgisCanvasRenderFunction = (\n    api: QgisApi,\n    srid: string,\n    xMin: number,\n    yMin: number,\n    xMax: number,\n    yMax: number,\n    width: number,\n    height: number,\n    pixelRatio: number,\n    layerIds?: string[],\n  ) => {\n    return api.renderImage(\n      srid,\n      new api.QgsRectangle(xMin, yMin, xMax, yMax),\n      width,\n      height,\n      pixelRatio,\n      layerIds,\n    );\n  };\n\n  protected renderFunction: QgisCanvasRenderFunction | undefined;\n  protected layerIds: string[] | undefined;\n\n  protected getrenderFunction(): QgisCanvasRenderFunction {\n    return this.renderFunction || QgisCanvasDataSource.DEFAULT_RENDERFUNCTION;\n  }\n\n  constructor(api: QgisApi, options: QgisCanvasDataSourceOptions = {}) {\n    super({\n      loader: (extent, resolution, requestPixelRatio) => {\n        return new Promise(async (resolve) => {\n          // note: requestPixelRatio is managed by ol and will not change on zoom\n          const pixelRatio = requestPixelRatio || window?.devicePixelRatio || 1;\n          const imageResolution = resolution / pixelRatio;\n          const width = Math.round(getWidth(extent) / imageResolution);\n          const height = Math.round(getHeight(extent) / imageResolution);\n\n          const renderFunction = this.getrenderFunction();\n          const imageData = await renderFunction(\n            this.api,\n            this.getProjection()?.getCode() || \"EPSG:3857\",\n            extent[0],\n            extent[1],\n            extent[2],\n            extent[3],\n            width,\n            height,\n            pixelRatio,\n            this.layerIds,\n          );\n\n          resolve(createImageBitmap(imageData));\n        });\n      },\n      ...options,\n    });\n\n    this.api = api;\n    this.renderFunction = options.renderFunction;\n    this.layerIds = options.layerIds;\n  }\n}\n"
  },
  {
    "path": "packages/qgis-js-ol/src/QgisJobDataSource.ts",
    "content": "import type { QgisApi, QgsMapRendererJob } from \"qgis-js\";\n\nimport ImageSource, { Options } from \"ol/source/Image\";\nimport ImageWrapper from \"ol/Image\";\nimport { getWidth, getHeight, getCenter, containsExtent } from \"ol/extent\";\nimport {\n  create as createTransform,\n  compose as composeTransform,\n} from \"ol/transform\";\n\nexport interface QgisJobDataSourceOptions extends Options {\n  /**\n   * Optional array of layer IDs to render. If omitted, renders all visible layers.\n   */\n  layerIds?: string[];\n\n  /**\n   * Specifies whether to enable preview mode.\n   * (default: true)\n   */\n  preview?: boolean;\n\n  /**\n   * Specifies the timeout to wait before rendering the next preview (in milliseconds).\n   * (default: 200)\n   */\n  previewTimeout?: number;\n\n  /**\n   * Specifies whether to enable overlay of the last fully rendered image on top of the previews.\n   * (default: true)\n   */\n  previewOverlay?: boolean;\n}\n\nexport class QgisJobDataSource extends ImageSource {\n  protected api: QgisApi;\n  protected layerIds: string[] | undefined;\n\n  protected preview: boolean;\n  protected previewTimeout: number;\n  protected previewOverlay: boolean;\n\n  private lastImage: ImageWrapper | null = null;\n\n  private jobs: QgsMapRendererJob[] = [];\n\n  constructor(api: QgisApi, options: QgisJobDataSourceOptions = {}) {\n    super({\n      loader: (extent, resolution, requestPixelRatio) => {\n        return new Promise(async (resolve) => {\n          this.dispatchEvent(\"jobstart\");\n\n          this.killPendingJobs();\n\n          const pixelRatio = requestPixelRatio || window?.devicePixelRatio || 1;\n          const imageResolution = resolution / pixelRatio;\n          const width = Math.round(getWidth(extent) / imageResolution);\n          const height = Math.round(getHeight(extent) / imageResolution);\n\n          const canvas = document.createElement(\"canvas\");\n\n          canvas.width = width;\n          canvas.height = height;\n          const ctx = canvas.getContext(\"2d\");\n\n          const job = api.renderJob(\n            this.getProjection()?.getCode() || \"EPSG:3857\",\n            new api.QgsRectangle(extent[0], extent[1], extent[2], extent[3]),\n            width,\n            height,\n            pixelRatio,\n            this.layerIds,\n          );\n          this.jobs.push(job);\n\n          const putRenderedImage = () => {\n            const data = new Uint8ClampedArray(\n              job.renderedImage(),\n            ) as Uint8ClampedArray<ArrayBuffer>;\n            const imageData = new ImageData(data, width, height);\n            ctx!.putImageData(imageData, 0, 0);\n          };\n\n          if (this.preview) {\n            const schedulePreivew = () => {\n              requestAnimationFrame(() => {\n                if (job.isActive()) {\n                  // if the preview will be entirely overlaid (e.g. on zooming in), we can skip preview rendering\n                  const skipPreviewRendering =\n                    this.previewOverlay &&\n                    this.lastImage &&\n                    containsExtent(this.lastImage.getExtent(), extent);\n\n                  if (!skipPreviewRendering) {\n                    putRenderedImage();\n                  }\n\n                  if (this.previewOverlay && this.lastImage) {\n                    const lastImageToDraw = this.lastImage.getImage();\n                    if (lastImageToDraw) {\n                      const imageExtent = this.lastImage.getExtent();\n                      const imageResolution = this.lastImage.getResolution();\n                      const [imageResolutionX, imageResolutionY] =\n                        Array.isArray(imageResolution)\n                          ? imageResolution\n                          : [imageResolution, imageResolution];\n                      const imagePixelRatio = this.lastImage.getPixelRatio();\n\n                      const viewCenter = getCenter(extent);\n\n                      const scaleX =\n                        (pixelRatio * imageResolutionX) /\n                        (resolution * imagePixelRatio);\n                      const scaleY =\n                        (pixelRatio * imageResolutionY) /\n                        (resolution * imagePixelRatio);\n\n                      const tempTransform = createTransform();\n                      const transform = composeTransform(\n                        tempTransform,\n                        width / 2,\n                        height / 2,\n                        scaleX,\n                        scaleY,\n                        0,\n                        (imagePixelRatio * (imageExtent[0] - viewCenter[0])) /\n                          imageResolutionX,\n                        (imagePixelRatio * (viewCenter[1] - imageExtent[3])) /\n                          imageResolutionY,\n                      );\n\n                      const dw = lastImageToDraw.width * transform[0];\n                      const dh = lastImageToDraw.height * transform[3];\n\n                      const dx = transform[4];\n                      const dy = transform[5];\n\n                      ctx!.drawImage(\n                        lastImageToDraw,\n                        0,\n                        0,\n                        +lastImageToDraw.width,\n                        +lastImageToDraw.height,\n                        dx,\n                        dy,\n                        dw,\n                        dh,\n                      );\n                    }\n                  }\n\n                  this.changed();\n\n                  resolve(canvas); // will have no effect if the canvas is already resolved\n\n                  // schedule the next preview if necessary and the job is still active\n                  if (!skipPreviewRendering && job.isActive()) {\n                    setTimeout(schedulePreivew, this.previewTimeout);\n                  }\n                }\n              });\n            };\n            schedulePreivew();\n          }\n\n          job.finished(() => {\n            putRenderedImage();\n\n            // store the current canvas to reuse it in upcoming previews\n            if (this.preview && this.previewOverlay) {\n              this.lastImage = this.image;\n            }\n\n            this.changed();\n\n            resolve(canvas); // will have no effect if the canvas is already resolved\n\n            // remove the job from the list of pending jobs\n            this.jobs = this.jobs.filter((j) => j !== job);\n\n            this.dispatchEvent(\"jobend\");\n          });\n        });\n      },\n      ...options,\n    });\n\n    this.api = api;\n    this.layerIds = options.layerIds;\n    this.preview =\n      typeof options.preview !== \"undefined\" ? options.preview : true;\n    this.previewTimeout =\n      typeof options.previewTimeout !== \"undefined\"\n        ? options.previewTimeout\n        : 200;\n    this.previewOverlay =\n      typeof options.previewOverlay !== \"undefined\"\n        ? options.previewOverlay\n        : true;\n  }\n\n  public killPendingJobs() {\n    while (this.jobs.length > 0) {\n      const job = this.jobs.pop();\n      if (job && job.isActive()) {\n        new Promise((resolve) => {\n          job.cancelWithoutBlocking();\n          resolve(null);\n        });\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/qgis-js-ol/src/QgisXYZDataSource.ts",
    "content": "import type { QgisApi } from \"qgis-js\";\n\nimport XYZ, { Options } from \"ol/source/XYZ\";\n\nimport { createCanvasContext2D } from \"ol/dom\";\nimport { toSize } from \"ol/size\";\n\nimport type { TileCoord } from \"ol/tilecoord\";\nimport ImageTile from \"ol/ImageTile\";\n\nexport interface QgisXYZDataSourceOptions extends Options {\n  extentBufferFactor?: number | (() => number);\n  layerIds?: string[];\n  renderFunction?: QgisXYZRenderFunction;\n  debug?: boolean;\n}\n\nexport type QgisXYZRenderFunction = (\n  api: QgisApi,\n  tileCoord: TileCoord,\n  tileSize: number,\n  pixelRatio: number,\n  extentBufferFactor: number,\n  layerIds?: string[],\n) => Promise<ImageData>;\n\nexport class QgisXYZDataSource extends XYZ {\n  protected api: QgisApi;\n\n  protected static DEFAULT_RENDERFUNCTION: QgisXYZRenderFunction = (\n    api: QgisApi,\n    tileCoord: TileCoord,\n    tileSize: number,\n    devicePixelRatio: number,\n    extentBufferFactor: number,\n    layerIds?: string[],\n  ) => {\n    return api.renderXYZTile(\n      tileCoord[1],\n      tileCoord[2],\n      tileCoord[0],\n      tileSize,\n      devicePixelRatio,\n      extentBufferFactor,\n      layerIds,\n    );\n  };\n\n  protected renderFunction: QgisXYZRenderFunction | undefined;\n  protected layerIds: string[] | undefined;\n\n  protected getrenderFunction(): QgisXYZRenderFunction {\n    return this.renderFunction || QgisXYZDataSource.DEFAULT_RENDERFUNCTION;\n  }\n\n  protected static DEFAULT_EXTENTBUFFERFACTOR = 0;\n\n  protected extentBufferFactor: number | number | (() => number) | undefined;\n\n  protected getextentBufferFactor(): number {\n    return typeof this.extentBufferFactor === \"function\"\n      ? this.extentBufferFactor()\n      : this.extentBufferFactor || QgisXYZDataSource.DEFAULT_EXTENTBUFFERFACTOR;\n  }\n\n  constructor(api: QgisApi, options: QgisXYZDataSourceOptions = {}) {\n    super({\n      tileUrlFunction: (tileCoord, pixelRatio) => {\n        const tileSize = (\n          toSize(this.tileGrid!.getTileSize(tileCoord[0])) as [number, number]\n        )[0];\n        return `${tileSize * (pixelRatio || 1)}`;\n      },\n      tileLoadFunction: async (tile, text) => {\n        const renderFunction = this.getrenderFunction();\n        if (this.tileGrid && renderFunction) {\n          console.assert(tile instanceof ImageTile);\n          const imageTile = tile as ImageTile;\n\n          const tileSize = parseInt(text);\n          const pixelRatio = Math.round(tileSize / 256);\n\n          const context = createCanvasContext2D(tileSize, tileSize);\n\n          const imageData = await renderFunction(\n            this.api,\n            tile.getTileCoord(),\n            tileSize,\n            pixelRatio,\n            this.getextentBufferFactor(),\n            this.layerIds,\n          );\n          context.putImageData(imageData, 0, 0);\n\n          if (options.debug) {\n            context.strokeStyle = \"grey\";\n            context.strokeRect(0.5, 0.5, tileSize + 0.5, tileSize + 0.5);\n\n            context.fillStyle = \"darkgrey\";\n            context.strokeStyle = \"black\";\n            context.textAlign = \"center\";\n            context.textBaseline = \"middle\";\n            context.font = \"20px sans-serif\";\n            context.lineWidth = 2;\n            context.strokeText(text, tileSize / 2, tileSize / 2, tileSize);\n            context.fillText(text, tileSize / 2, tileSize / 2, tileSize);\n          }\n\n          imageTile.setImage(context.canvas);\n        }\n      },\n      ...options,\n    });\n\n    this.api = api;\n    this.renderFunction = options.renderFunction;\n    this.extentBufferFactor = options.extentBufferFactor;\n    this.layerIds = options.layerIds;\n  }\n}\n"
  },
  {
    "path": "packages/qgis-js-ol/src/index.ts",
    "content": "export { QgisCanvasDataSource } from \"./QgisCanvasDataSource\";\nexport { QgisXYZDataSource } from \"./QgisXYZDataSource\";\nexport { QgisJobDataSource } from \"./QgisJobDataSource\";\n"
  },
  {
    "path": "packages/qgis-js-ol/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/qgis-js-ol/vite.config.ts",
    "content": "import { resolve } from \"path\";\nimport { defineConfig } from \"vite\";\n\nimport dts from \"vite-plugin-dts\";\n\nexport default defineConfig({\n  plugins: [\n    dts({\n      rollupTypes: true,\n      entryRoot: \"src\",\n    }),\n  ],\n  build: {\n    lib: {\n      entry: [resolve(__dirname, \"src/index.ts\")],\n      name: \"qgis-js-ol\",\n      formats: [\"es\"],\n      fileName: \"qgis-js-ol\",\n    },\n    rollupOptions: {\n      external: [\"qgis-js\", \"ol\", new RegExp(\"^ol/*\")],\n    },\n  },\n});\n"
  },
  {
    "path": "packages/qgis-js-utils/README.md",
    "content": "# @qgis-js/utils\n\n**Utilities to integrate [qgis-js](https://github.com/qgis/qgis-js) into web applications**\n\n[qgis-js Repository](https://github.com/qgis/qgis-js) | [qgis-js Website](https://qgis.github.io/qgis-js) | [\"`@qgis-js/utils`\" package source](https://github.com/qgis/qgis-js/tree/main/packages/qgis-js-ol)\n\n[![@qgis-js/utils on npm](https://img.shields.io/npm/v/@qgis-js/utils)](https://www.npmjs.com/package/@qgis-js/utils)\n\n> ⚠️🧪 **Work in progress**! Currently this project is in public beta\n\n## Installation\n\n```bash\nnpm install -S @qgis-js/utils\n```\n\n## Usage\n\n### `useProjects`\n\nProvides an abstraction to load QGIS projects from various sources.\n\n```js\nimport { qgis } from \"qgis-js\";\nimport { useProjects } from \"@qgis-js/utils\";\n\nconst { api, fs } = await qgis();\nconst {\n  openProject,\n  loadLocalProject,\n  loadGithubProjects,\n  loadRemoteProjects,\n} = useProjects(fs, (projectPath: string) => {\n  api.loadProject(projectPath);\n});\n```\n\nThe following project sources are supported:\n\n#### LocalProject\n\nLoads QGIS projects from the user's file system with the [File System API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API)\n\n```js\nawait openProject(await loadLocalProject());\n```\n\n#### GithubProject\n\nLoads QGIS projects from a GitHub repository with the [GitHub API](https://docs.github.com/en/rest)\n\n#### RemoteProject\n\nFetches QGIS projects from a remote server with the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)\n\n- If `loadRemoteProjects` is invoked with a string as, it is assumed to be the URL of a JSON file with the following structure:\n\n```json\n{\n  \"name\": \"projects\",\n  \"path\": \"projects\",\n  \"type\": \"Folder\",\n  \"entries\": [\n    {\n      \"name\": \"village\",\n      \"path\": \"projects/village\",\n      \"type\": \"Folder\",\n      \"entries\": [\n        {\n          \"name\": \"project.qgs\",\n          \"path\": \"projects/village/project.qgs\",\n          \"type\": \"File\"\n        },\n        {\n          \"name\": \"rgb.tif\",\n          \"path\": \"projects/village/rgb.tif\",\n          \"type\": \"File\"\n        }\n      ]\n    }\n  ]\n}\n```\n\n- Otherwise a `Folder` object can also be passed directly to `loadRemoteProjects`, see [FileSystem.ts](./src/fs/FileSystem.ts)\n\n## Versioning\n\nThis package uses [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/qgis/qgis-js/tags).\n\n## License\n\n[GNU General Public License v2.0](https://github.com/qgis/qgis-js/blob/main/LICENSE)\n"
  },
  {
    "path": "packages/qgis-js-utils/package.json",
    "content": "{\n  \"name\": \"@qgis-js/utils\",\n  \"version\": \"4.0.0\",\n  \"description\": \"Utilities to integrate qgis-js into web applications\",\n  \"license\": \"GPL-2.0-or-later\",\n  \"homepage\": \"https://qgis.github.io/qgis-js/\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/qgis/qgis-js\",\n    \"directory\": \"packages/qgis-js-utils\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/qgis/qgis-js/issues\"\n  },\n  \"type\": \"module\",\n  \"main\": \"dist/qgis-js-utils.js\",\n  \"types\": \"dist/qgis-js-utils.d.ts\",\n  \"files\": [\n    \"dist/**/*\"\n  ],\n  \"scripts\": {\n    \"build\": \"vite build\"\n  },\n  \"dependencies\": {\n    \"@types/emscripten\": \"1.41.5\",\n    \"browser-fs-access\": \"0.38.0\",\n    \"qgis-js\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^22.19.15\",\n    \"typescript\": \"5.8.2\",\n    \"vite\": \"8.0.0\",\n    \"vite-plugin-dts\": \"4.5.4\"\n  }\n}\n"
  },
  {
    "path": "packages/qgis-js-utils/src/fs/FileSystem.ts",
    "content": "export type FileSystemEntryType = \"File\" | \"Folder\";\n\nexport interface FileSystemEntry {\n  name: string;\n  path: string;\n  type: FileSystemEntryType;\n}\n\nexport interface File extends FileSystemEntry {}\n\nexport interface Folder extends FileSystemEntry {\n  entries: FolderEntries;\n}\n\nexport type FolderEntries = Array<File | Folder>;\n\n/**\n * Flattens the \"Folder\" hierarchy and returns an array of folder paths.\n *\n * @param entries - The entries of the root folder.\n * @param basePath - The base path of the root folder.\n * @returns An array of folder paths.\n */\nexport function flatFolders(\n  entries: Folder[\"entries\"],\n  basePath: string,\n): string[] {\n  return entries.reduce<string[]>((acc, entry) => {\n    if (entry.type === \"Folder\") {\n      return acc.concat([\n        basePath + \"/\" + entry.name,\n        ...flatFolders((entry as Folder).entries, basePath + \"/\" + entry.name),\n      ]);\n    } else {\n      return acc;\n    }\n  }, []);\n}\n\n/**\n * Flattens the files in a \"Folder\" hierarchy and returns an array of file paths.\n *\n * @param entries - The entries of the root folder.\n * @param basePath - The base path of the root folder.\n * @returns An array of file paths.\n */\nexport function flatFiles(\n  entries: Folder[\"entries\"],\n  basePath: string,\n): string[] {\n  return entries.reduce<string[]>((acc, entry) => {\n    if (entry.type === \"Folder\") {\n      return acc.concat(\n        flatFiles((entry as Folder).entries, basePath + \"/\" + entry.name),\n      );\n    } else {\n      return acc.concat(basePath + \"/\" + entry.name);\n    }\n  }, []);\n}\n"
  },
  {
    "path": "packages/qgis-js-utils/src/fs/GithubProject.ts",
    "content": "import type { EmscriptenFS } from \"qgis-js\";\n\nimport { Project, PROJECTS_UPLOAD_DIR } from \"./Project\";\n\nimport { Folder, flatFolders, flatFiles } from \"./FileSystem\";\n\nexport const GIT_DEFAULT_BRANCH = \"main\";\n\n/**\n * Fetches the contents of a directory in a GitHub repository via the GitHub REST API.\n *\n * Be aware of the GitHub API rate limits: https://docs.github.com/en/rest/overview/rate-limits-for-the-rest-api\n *\n * @param owner The owner of the repository.\n * @param repo The name of the repository.\n * @param path The path to the directory to fetch. Defaults to the root directory.\n * @param branch The branch to fetch the directory from. Defaults to the default branch of the repository.\n * @returns A Promise that resolves to an array of objects representing the files and directories in the specified directory.\n */\nexport async function fetchGithubDirectory(\n  owner: string,\n  repo: string,\n  path: string = \"/\",\n  branch: string = GIT_DEFAULT_BRANCH,\n) {\n  const url = `https://api.github.com/repos/${owner}/${repo}/contents${path}?ref=${branch}`;\n  const response = await fetch(url);\n  const content = (await response.json()) as Array<{\n    type: \"file\" | \"dir\";\n    name: string;\n    path: string;\n    sha: string;\n  }>;\n  return content;\n}\n\n/**\n * Fetches a list of relative file paths in a GitHub repository tree via the GitHub REST API.\n *\n * Be aware of the GitHub API rate limits: https://docs.github.com/en/rest/overview/rate-limits-for-the-rest-api\n *\n * @param owner The owner of the repository.\n * @param repo The name of the repository.\n * @param sha The SHA of the tree to fetch the files from.\n * @returns A Promise that resolves to an array of relative file paths in the specified tree.\n */\nexport async function fetchGithubTreeFiles(\n  owner: string,\n  repo: string,\n  sha: string,\n) {\n  const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/${sha}?recursive=1`;\n  const response = await fetch(url);\n  const content = (await response.json()) as {\n    tree: {\n      type: \"tree\" | \"blob\";\n      path: string;\n    }[];\n  };\n  return content.tree\n    .filter((entry) => entry.type === \"blob\")\n    .map((entry) => entry.path);\n}\n\n/**\n * Fetches the content of a file from a GitHub repository via the GitHub REST API.\n *\n * Be aware of the GitHub API rate limits: https://docs.github.com/en/rest/overview/rate-limits-for-the-rest-api\n *\n * @param owner The owner of the repository.\n * @param repo The name of the repository.\n * @param path The path to the file. Defaults to the root directory.\n * @param branch The branch to fetch the file from. Defaults to the default branch of the repository.\n * @returns A Promise that resolves to the file content as a buffer.\n * @throws An error if the file content is not found.\n */\nexport async function fetchGithubFileContent(\n  owner: string,\n  repo: string,\n  path: string = \"/\",\n  branch: string = GIT_DEFAULT_BRANCH,\n) {\n  const url = `https://api.github.com/repos/${owner}/${repo}/contents${path}?ref=${branch}`;\n  const response = await fetch(url);\n  const file = (await response.json()) as {\n    type: \"file\" | \"dir\";\n    content?: string;\n    download_url?: string;\n  };\n  if (file.content && file.content !== \"\") {\n    return Uint8Array.from(atob(file.content), (c) => c.charCodeAt(0)).buffer;\n  } else if (file.download_url && file.download_url !== \"\") {\n    const response = await fetch(file.download_url);\n    return response.arrayBuffer();\n  } else {\n    throw new Error(\"File content of \" + path + \" not found.\");\n  }\n}\n\n/**\n * Maps an array of file paths to a \"Folder\" structure.\n *\n * @param name - The name of the root folder.\n * @param path - The path of the root folder.\n * @param files - An array of relative file paths.\n * @returns The root folder with the mapped \"Folder\" structure.\n */\nexport function mapFilesToFolder(\n  name: string,\n  path: string,\n  files: string[],\n): Folder {\n  const root: File | Folder = { name, path, type: \"Folder\", entries: [] };\n  files.sort().forEach((path) => {\n    const parts = path.split(\"/\");\n    let current = root;\n    for (let i = 0; i < parts.length - 1; i++) {\n      const part = parts[i];\n      const entry = current.entries.find((entry) => entry.name === part);\n      if (entry) {\n        current = entry as Folder;\n      } else {\n        const folder: Folder = {\n          name: part,\n          path: current.path + \"/\" + part,\n          type: \"Folder\",\n          entries: [],\n        };\n        current.entries.push(folder);\n        current = folder;\n      }\n    }\n    current.entries.push({\n      name: parts[parts.length - 1],\n      path,\n      type: \"File\",\n    });\n  });\n  return root;\n}\n\nexport class GithubProject extends Project {\n  protected folder: Folder;\n  protected path: string;\n\n  protected owner: string;\n  protected repo: string;\n  protected branch: string;\n\n  constructor(\n    FS: EmscriptenFS,\n    projectFolder: Folder,\n    owner: string,\n    repo: string,\n    branch: string = GIT_DEFAULT_BRANCH,\n  ) {\n    super(FS, \"Github\");\n\n    this.folder = projectFolder;\n\n    this.name = projectFolder.name;\n    this.path = projectFolder.path;\n\n    this.owner = owner;\n    this.repo = repo;\n    this.branch = branch;\n  }\n\n  getDirectories(): string[] {\n    return [this.path, ...flatFolders(this.folder.entries, this.path)];\n  }\n\n  getFiles(): string[] {\n    return flatFiles(this.folder.entries, this.path);\n  }\n\n  async uploadProject() {\n    // prepare to download all files in parallel\n    const downloads = Object.fromEntries(\n      this.getFiles().map((file) => {\n        return [\n          file,\n          new Promise<ArrayBuffer>(async (resolve, _reject) => {\n            // note that we don't use fetchGithubFileContent here because of the GitHub API rate limits\n            // instead we use the raw.githubusercontent.com URL directly\n            const url = `https://raw.githubusercontent.com/${this.owner}/${this.repo}/${this.branch}/${file}`;\n            const response = await fetch(url);\n            resolve(response.arrayBuffer());\n          }),\n        ];\n      }),\n    );\n    // wait for all the responses\n    await Promise.all([Object.values(downloads)]);\n    // ensure directories exist\n    this.ensureDirectories();\n    // write files to the runtime FS\n    for (const file of this.getFiles()) {\n      const data = new Uint8Array(await downloads[file]);\n      this.FS.writeFile(PROJECTS_UPLOAD_DIR + \"/\" + file, data);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/qgis-js-utils/src/fs/LocalProject.ts",
    "content": "import type { EmscriptenFS } from \"qgis-js\";\n\nimport { Project, PROJECTS_UPLOAD_DIR } from \"./Project\";\n\nimport {\n  directoryOpen,\n  FileWithDirectoryAndFileHandle,\n} from \"browser-fs-access\";\n\nexport type LocalEntries = Array<FileWithDirectoryAndFileHandle>;\n\nexport { directoryOpen as openLocalDirectory };\n\nexport class LocalProject extends Project {\n  protected entries: LocalEntries;\n\n  constructor(FS: EmscriptenFS, entries: LocalEntries) {\n    super(FS, \"Local\");\n\n    this.entries = entries;\n\n    const possibleNames = entries\n      .map((e) => e.webkitRelativePath)\n      .filter((p) => p && p.length > 0 && p.includes(\"/\"))\n      .map((p) => p.split(\"/\", 1)[0]);\n\n    if (possibleNames.length < 1) {\n      throw new Error(\"Could not determine project name\");\n    }\n    // just use the first possible name as final project name\n    this.name = possibleNames[0];\n  }\n\n  getDirectories(): string[] {\n    const subFoldersToCreate = new Set<string>();\n    subFoldersToCreate.add(this.name);\n    for (const entry of this.entries) {\n      const path = (entry as FileWithDirectoryAndFileHandle).webkitRelativePath;\n      subFoldersToCreate.add(path.substring(0, path.lastIndexOf(\"/\")));\n    }\n    return Array.from(subFoldersToCreate);\n  }\n\n  getFiles(): string[] {\n    return this.entries.map((e) => e.webkitRelativePath);\n  }\n\n  async uploadProject(): Promise<void> {\n    // ensure directories exist\n    this.ensureDirectories();\n    // write files to the runtime FS\n    for (const file of this.entries) {\n      this.FS.writeFile(\n        PROJECTS_UPLOAD_DIR + \"/\" + file.webkitRelativePath,\n        new Uint8Array(await file.arrayBuffer()),\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/qgis-js-utils/src/fs/Project.ts",
    "content": "import type { EmscriptenFS } from \"qgis-js\";\n\nexport type ProjectType = \"Remote\" | \"Local\" | \"Github\";\n\nexport const PROJECTS_UPLOAD_DIR = \"/upload/projects\";\n\nexport abstract class Project {\n  protected FS;\n  type: ProjectType;\n  name!: string;\n\n  constructor(FS: EmscriptenFS, type: ProjectType) {\n    this.FS = FS;\n    this.type = type;\n  }\n\n  abstract getDirectories(): string[];\n  abstract getFiles(): string[];\n\n  abstract uploadProject(): Promise<void>;\n\n  isValid(): boolean {\n    return this.getFiles().length > 0 && this.getProjectFile() !== undefined;\n  }\n\n  getProjectFile(): string | undefined {\n    const candidates = this.getFiles().filter(\n      (f) => f.endsWith(\".qgs\") || f.endsWith(\".qgz\"),\n    );\n    if (candidates.length == 1) {\n      return candidates[0];\n    } else if (candidates.length > 1) {\n      console.warn(\"Found multiple project file candiates\");\n      return candidates[0];\n    } else {\n      return undefined;\n    }\n  }\n\n  getDirectoriesToCreate(): string[] {\n    const directories = new Set<string>();\n    for (const directory of this.getDirectories()) {\n      let direcotryDirs = directory.split(\"/\");\n      for (let i = 0; i < direcotryDirs.length; i++) {\n        const dirToCreate = direcotryDirs.slice(0, i + 1).join(\"/\");\n        directories.add(dirToCreate);\n      }\n    }\n    return Array.from(directories).sort();\n  }\n\n  ensureDirectories() {\n    // create directories in the runtime FS (if not already existing)\n    for (const directory of this.getDirectoriesToCreate()) {\n      const dirToCreate = PROJECTS_UPLOAD_DIR + \"/\" + directory;\n      // @ts-ignore (FS by @types/emscripten is missing the analyzePath method...)\n      const node = this.FS.analyzePath(dirToCreate, false);\n      // @ts-ignore\n      if (!node || !node.exists) {\n        this.FS.mkdir(dirToCreate);\n      }\n    }\n  }\n\n  isProjectUploaded() {\n    return this.FS.readdir(PROJECTS_UPLOAD_DIR).includes(this.name);\n  }\n}\n"
  },
  {
    "path": "packages/qgis-js-utils/src/fs/RemoteProject.ts",
    "content": "import type { EmscriptenFS } from \"qgis-js\";\n\nimport { Project, PROJECTS_UPLOAD_DIR } from \"./Project\";\n\nimport { Folder, flatFolders, flatFiles } from \"./FileSystem\";\n\nexport class RemoteProject extends Project {\n  protected folder: Folder;\n  protected path: string;\n\n  private baseUrl: URL;\n\n  constructor(FS: EmscriptenFS, basePath: string, projectFolder: Folder) {\n    super(FS, \"Remote\");\n\n    this.baseUrl = new URL(basePath);\n    this.folder = projectFolder;\n\n    const bsaeFolder = this.baseUrl.pathname.split(\"/\").pop();\n\n    this.name = projectFolder.name;\n    this.path = projectFolder.path.replace(new RegExp(`^${bsaeFolder}\\/`), \"\");\n  }\n\n  getDirectories(): string[] {\n    return [this.path, ...flatFolders(this.folder.entries, this.path)];\n  }\n\n  getFiles(): string[] {\n    return flatFiles(this.folder.entries, this.path);\n  }\n\n  async uploadProject() {\n    // download all files in parallel\n    const downloads = Object.fromEntries(\n      this.getFiles().map((file) => {\n        return [\n          file,\n          new Promise<ArrayBuffer>((resolve, _reject) => {\n            const remoteUrl = new URL(`${this.baseUrl.href}/${file}`);\n            fetch(remoteUrl).then((response) =>\n              response.arrayBuffer().then((buffer) => {\n                resolve(buffer);\n              }),\n            );\n          }),\n        ];\n      }),\n    );\n    // wait for all responses\n    await Promise.all([Object.values(downloads)]);\n    // ensure directories exist\n    this.ensureDirectories();\n    // write files to the runtime FS\n    for (const file of this.getFiles()) {\n      const data = new Uint8Array(await downloads[file]);\n      this.FS.writeFile(PROJECTS_UPLOAD_DIR + \"/\" + file, data);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/qgis-js-utils/src/fs/index.ts",
    "content": "import type { EmscriptenFS } from \"qgis-js\";\n\nimport { Folder } from \"./FileSystem\";\n\nimport { PROJECTS_UPLOAD_DIR, Project } from \"./Project\";\nimport { LocalEntries, LocalProject, openLocalDirectory } from \"./LocalProject\";\nimport { RemoteProject } from \"./RemoteProject\";\nimport {\n  GithubProject,\n  fetchGithubDirectory,\n  fetchGithubTreeFiles,\n  mapFilesToFolder,\n} from \"./GithubProject\";\n\nexport function useProjects(\n  fs: EmscriptenFS,\n  onProjectLoaded: (projectFile: string) => void,\n) {\n  // ensure PROJECTS_UPLOAD_DIR (and its parent dirs) exist\n  let projectUploadDirs = PROJECTS_UPLOAD_DIR.split(\"/\");\n  for (let i = 1; i < projectUploadDirs.length; i++) {\n    fs.mkdir(projectUploadDirs.slice(0, i + 1).join(\"/\"));\n  }\n\n  const openProject = async (project: Project | Promise<Project>) => {\n    if (!project) {\n      return;\n    } else {\n      if (project instanceof Promise) {\n        project = await project;\n      }\n\n      if (!project.isValid()) {\n        throw new Error(`Project \"${project.name}\" is not valid`);\n      }\n      if (!project.isProjectUploaded()) {\n        await project.uploadProject();\n      }\n      loadProject(project);\n    }\n  };\n\n  const loadProject = (project: Project) => {\n    const projectFile = PROJECTS_UPLOAD_DIR + \"/\" + project.getProjectFile();\n    onProjectLoaded(projectFile);\n  };\n\n  const loadLocalProject = () =>\n    new Promise<LocalProject>(async (resolve, reject) => {\n      try {\n        const entries: LocalEntries = (await openLocalDirectory({\n          recursive: true,\n          mode: \"read\",\n        })) as LocalEntries; //TODO: This cast is probably not working when \"fs-browser-fs-access\" is using the fallback implementation\n        const localProject = new LocalProject(fs, entries);\n        resolve(localProject);\n      } catch (error) {\n        reject(error);\n      }\n    });\n\n  const loadRemoteProjects = (\n    remoteProjects: string = \"./projects/directory-listing.json\",\n  ) =>\n    new Promise<RemoteProject[]>(async (resolve, reject) => {\n      try {\n        const url = new URL(remoteProjects as string, window.location.href);\n        const basePath = url.href.split(\"/\").slice(0, -1).join(\"/\");\n\n        const remoteProjectsResponse = await fetch(remoteProjects);\n        const remoteProjectsResponseJson = await remoteProjectsResponse.json();\n\n        const remoteFolder = remoteProjectsResponseJson as Folder;\n        if (!remoteFolder.type || remoteFolder.type !== \"Folder\") {\n          reject(new Error(\"Remote projects response seems not a folder\"));\n          return;\n        }\n\n        resolve(\n          remoteFolder.entries\n            .filter((entry) => entry.type === \"Folder\")\n            .map((entry) => new RemoteProject(fs, basePath, entry as Folder)),\n        );\n      } catch (error) {\n        reject(error);\n      }\n    });\n\n  const loadGithubProjects = (\n    owner: string,\n    repo: string,\n    path: string = \"/\",\n    branch: string = \"main\",\n  ) =>\n    new Promise<{ [key: string]: () => Promise<GithubProject> }>(\n      async (resolve, reject) => {\n        try {\n          const projects = await fetchGithubDirectory(\n            owner,\n            repo,\n            path,\n            branch,\n          );\n          // check if the response got an error message\n          if (!(projects instanceof Array)) {\n            console.warn(projects);\n            resolve({});\n            return;\n          } else {\n            resolve(\n              Object.fromEntries(\n                projects\n                  .filter((entry) => entry.type === \"dir\")\n                  .map((entry) => {\n                    return [\n                      entry.name,\n                      () => {\n                        return new Promise<GithubProject>(async (resolve) => {\n                          const files = await fetchGithubTreeFiles(\n                            owner,\n                            repo,\n                            entry.sha,\n                          );\n                          resolve(\n                            new GithubProject(\n                              fs,\n                              mapFilesToFolder(entry.name, entry.path, files),\n                              owner,\n                              repo,\n                              branch,\n                            ),\n                          );\n                        });\n                      },\n                    ];\n                  }),\n              ),\n            );\n          }\n        } catch (error) {\n          reject(error);\n        }\n      },\n    );\n\n  return {\n    openProject,\n    loadLocalProject,\n    loadRemoteProjects,\n    loadGithubProjects,\n  };\n}\n"
  },
  {
    "path": "packages/qgis-js-utils/src/index.ts",
    "content": "export { useProjects } from \"./fs\";\n\nexport type { Project } from \"./fs/Project\";\nexport type { FileSystemEntry, File, Folder } from \"./fs/FileSystem\";\n"
  },
  {
    "path": "packages/qgis-js-utils/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "packages/qgis-js-utils/vite.config.ts",
    "content": "import { resolve } from \"path\";\nimport { defineConfig } from \"vite\";\n\nimport dts from \"vite-plugin-dts\";\n\nexport default defineConfig({\n  plugins: [\n    dts({\n      rollupTypes: true,\n      entryRoot: \"src\",\n    }),\n  ],\n  build: {\n    lib: {\n      entry: [resolve(__dirname, \"src/index.ts\")],\n      name: \"qgis-js-utils\",\n      formats: [\"es\"],\n      fileName: \"qgis-js-utils\",\n    },\n    rollupOptions: {\n      external: [\"qgis-js\", \"public/projects\"],\n    },\n  },\n});\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - packages/*\n  - sites/*\nonlyBuiltDependencies:\n  - esbuild\n"
  },
  {
    "path": "qgis-js.ts",
    "content": "#!/usr/bin/env -S node_modules/.bin/vite-node --script\n\n/*\n * qgis-js CLI\n *\n * Will help you to build qgis-js and generate its documentation.\n *\n * Run \"qgis-js --help\" for more information.\n */\n\nimport { CommandLineParser } from \"@rushstack/ts-command-line\";\n\nimport { CleanAction } from \"./build/actions/clean\";\nimport { InstallAction } from \"./build/actions/install\";\nimport { CompileAction } from \"./build/actions/compile\";\nimport { LibsAction } from \"./build/actions/libs\";\nimport { SizeAction } from \"./build/actions/size\";\n\nimport { QgisJsOptions } from \"./build/actions/lib/QgisJsOptions\";\n\nconst options: QgisJsOptions = {} as QgisJsOptions;\n\nexport class QgisJsCommandLine extends CommandLineParser {\n  public constructor() {\n    super({\n      toolFilename: \"qgis-js\",\n      toolDescription: 'The \"qgis-js\" build tool.',\n    });\n    this.addAction(new CleanAction(options));\n    this.addAction(new InstallAction(options));\n    this.addAction(new CompileAction(options));\n    this.addAction(new LibsAction(options));\n    this.addAction(new SizeAction(options));\n\n    this.defineFlagParameter({\n      parameterLongName: \"--verbose\",\n      parameterShortName: \"-v\",\n      description: \"Show extra logging detail\",\n    });\n  }\n\n  protected onExecuteAsync(): Promise<void> {\n    options.verbose = this.getFlagParameter(\"--verbose\")?.value || false;\n\n    process.env.FORCE_COLOR = \"1\";\n\n    return super.onExecuteAsync();\n  }\n}\n\nconst qgisjs = new QgisJsCommandLine();\nqgisjs.executeAsync();\n"
  },
  {
    "path": "sites/dev/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <title>qgis-js</title>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"qgis-icon128.svg\" />\n    <link href=\"src/demo.css\" rel=\"stylesheet\" type=\"text/css\" />\n    <script>\n      if (window[\"SharedArrayBuffer\"] === undefined) {\n        console.log(\"Installing COOP/COEP service worker\");\n\n        window.coi = {\n          shouldRegister: () => true,\n          shouldDeregister: () => true,\n          coepCredentialless: () => !(window.chrome || window.netscape),\n          quiet: true,\n        };\n\n        const coiServiceworker = document.createElement(\"script\");\n        coiServiceworker.setAttribute(\"src\", \"coi-serviceworker.min.js\");\n        document.head.appendChild(coiServiceworker);\n      }\n    </script>\n  </head>\n\n  <body>\n    <h1>qgis-js</h1>\n    <h3>QGIS core ported to WebAssembly to run it on the web platform</h3>\n    <br />\n    <div class=\"tabset\">\n      <input\n        type=\"radio\"\n        name=\"tabset\"\n        id=\"tab0\"\n        aria-controls=\"openlayers_canvas\"\n        checked\n      />\n      <label for=\"tab0\">ol: Canvas Tile</label>\n      <input\n        type=\"radio\"\n        name=\"tabset\"\n        id=\"tab1\"\n        aria-controls=\"openlayers_preview\"\n      />\n      <label for=\"tab1\">ol: Preview</label>\n      <input\n        type=\"radio\"\n        name=\"tabset\"\n        id=\"tab2\"\n        aria-controls=\"openlayers_xyz\"\n      />\n      <label for=\"tab2\">ol: XYZ Tiles </label>\n      <input type=\"radio\" name=\"tabset\" id=\"tab3\" aria-controls=\"javascript\" />\n      <label for=\"tab3\">JavaScript</label>\n\n      <div id=\"status\">\n        <div id=\"status-message\">Initializing...</div>\n        <div id=\"status-progress\">\n          <div class=\"lds-ellipsis\">\n            <div></div>\n            <div></div>\n            <div></div>\n            <div></div>\n          </div>\n        </div>\n      </div>\n\n      <div id=\"project\" style=\"visibility: hidden\">\n        <div>\n          <label for=\"projects\">Project:</label>\n\n          <select name=\"projects\" id=\"projects\"></select>\n        </div>\n        <div>\n          <button id=\"local-project\">Add local project</button>\n        </div>\n      </div>\n\n      <div class=\"tab-panels\">\n        <section id=\"openlayers_canvas\" class=\"tab-panel\">\n          <h2>OpenLayers: Canvas Tile</h2>\n          <div id=\"ol-demo-canvas\" class=\"demo\"></div>\n          <p>\n            <a href=\"https://github.com/qgis/qgis-js\" target=\"_blank\"\n              >qgis-js</a\n            >\n            provides a custom &laquo;<strong>Canvas Tile</strong>&raquo;\n            <a href=\"https://openlayers.org/\" target=\"_blank\">OpenLayers</a>\n            source for rendering a single tile with the size and pixel ratio of\n            the ol map canvas. This is useful for rendering in the projection of\n            the QGIS project, both in the OpenLayers view and in the qgis-js\n            runtime.\n          </p>\n          <p>\n            See\n            <a\n              href=\"https://github.com/qgis/qgis-js/blob/main/packages/qgis-js-ol/src/QgisCanvasDataSource.ts\"\n              target=\"_blank\"\n              class=\"source\"\n              >QgisCanvasDataSource.ts</a\n            >\n            for the implementation and the\n            <a\n              href=\"https://github.com/qgis/qgis-js/blob/main/sites/dev/src/ol.ts#L130\"\n              target=\"_blank\"\n              class=\"source\"\n              >olDemoCanvas-function</a\n            >\n            for the demo setup used on this page.\n          </p>\n        </section>\n\n        <section id=\"openlayers_preview\" class=\"tab-panel\">\n          <h2>OpenLayers: Preview</h2>\n          <div id=\"ol-demo-preview\" class=\"demo\"></div>\n          <div class=\"canvas-options\">\n            <label for=\"previewRendering\">Preview rendering:</label>\n            <input\n              type=\"checkbox\"\n              checked=\"checked\"\n              name=\"previewRendering\"\n              id=\"previewRendering\"\n              onchange=\"\n                [\n                  ...document.querySelectorAll(\n                    '#previewTimeout, #previewOverlay',\n                  ),\n                ].map((e) => (e.disabled = !this.checked))\n              \"\n            />\n\n            <div class=\"seperator\">&nbsp;</div>\n\n            <label for=\"previewTimeout\">Preview timeout:</label>\n            <input\n              type=\"number\"\n              value=\"200\"\n              min=\"0\"\n              max=\"10000\"\n              name=\"previewTimeout\"\n              id=\"previewTimeout\"\n            />\n\n            <div class=\"seperator\">&nbsp;</div>\n\n            <label for=\"previewOverlay\">Preview overlay:</label>\n            <input\n              type=\"checkbox\"\n              checked=\"checked\"\n              name=\"previewOverlay\"\n              id=\"previewOverlay\"\n            />\n          </div>\n        </section>\n\n        <section id=\"openlayers_xyz\" class=\"tab-panel\">\n          <h2>OpenLayers: XYZ Tiles</h2>\n          <div id=\"ol-demo-xyz\" class=\"demo\"></div>\n          <div class=\"canvas-options\">\n            <label\n              title=\"Factor will be multiploed with the tiles width in map units.\"\n              for=\"extentBufferFactor\"\n              >Extent Buffer Factor:</label\n            >\n            <input\n              title=\"Factor will be multiploed with the tiles width in map units.\"\n              type=\"range\"\n              min=\"0\"\n              max=\"2\"\n              step=\"0.01\"\n              value=\"0.5\"\n              name=\"extentBufferFactor\"\n              id=\"extentBufferFactor\"\n              oninput=\"\n                this.nextElementSibling.value = (\n                  Math.round(this.value * 100) / 100\n                ).toFixed(2)\n              \"\n            />\n            <output>0.50</output>\n\n            <div class=\"seperator\">&nbsp;</div>\n\n            <label for=\"xyzBaseMap\">OSM Basemap:</label>\n            <input\n              type=\"checkbox\"\n              checked=\"checked\"\n              name=\"xyzBaseMap\"\n              id=\"xyzBaseMap\"\n            />\n          </div>\n          <p>\n            With the\n            <a href=\"https://github.com/qgis/qgis-js\" target=\"_blank\"\n              >qgis-js</a\n            >\n            custom <strong>XYZ Tiles</strong> source for\n            <a href=\"https://openlayers.org/\" target=\"_blank\">OpenLayers</a>,\n            the QGIS project is rendered in the common Web Mercator projection\n            (EPSG:3857) addressed with the xyz tile scheme. This makes it\n            convenient to mix the QGIS layer with other layer sources provided\n            by OpenLayers.\n          </p>\n          <p>\n            See\n            <a\n              href=\"https://github.com/qgis/qgis-js/blob/main/packages/qgis-js-ol/src/QgisXYZDataSource.ts\"\n              target=\"_blank\"\n              class=\"source\"\n              >QgisXYZDataSource.ts</a\n            >\n            for the implementation and the\n            <a\n              href=\"https://github.com/qgis/qgis-js/blob/main/sites/dev/src/ol.ts#L28\"\n              target=\"_blank\"\n              class=\"source\"\n              >olDemoXYZ-function</a\n            >\n            for the demo setup used on this page.\n          </p>\n        </section>\n\n        <section id=\"javascript\" class=\"tab-panel\">\n          <h2>JavaScript</h2>\n          <div id=\"js-demo\" class=\"demo\">\n            <canvas id=\"js-demo-canvas\"></canvas>\n\n            <div id=\"js-demo-controls\">\n              <button id=\"zoomin\">zoom in</button>\n              <button id=\"zoomout\">zoom out</button>\n              <button id=\"panleft\">left</button>\n              <button id=\"panright\">right</button>\n              <button id=\"panup\">up</button>\n              <button id=\"pandown\">down</button>\n            </div>\n          </div>\n          <p>\n            <strong>qgis-js</strong> can be used with plain JavaScript and can\n            therefore be integrated into any web application. The library\n            provides also type information and integrates nicely with\n            TypeScript.\n          </p>\n          <p>\n            The example above shows how to use qgis-js with a simple canvas\n            element and some rudimentary controls. The source code can be found\n            in\n            <a\n              href=\"https://github.com/qgis/qgis-js/blob/main/sites/dev/src/js.ts\"\n              target=\"_blank\"\n              class=\"source\"\n              >sites/dev/src/js.ts</a\n            >\n          </p>\n          <p>\n            The qgis-js API exposes also some QGIS core internals which can be\n            used to build web GIS applications:\n          </p>\n\n          <div class=\"code\">\n            <pre\n              style=\"margin: 0; line-height: 125%\"\n            ><span style=\"color: #589632; font-weight: bold\">const</span> rectangle <span style=\"color: #666666\">=</span> <span style=\"color: #589632; font-weight: bold\">new</span> api.QgsRectangle(<span style=\"color: #666666\">100</span>, <span style=\"color: #666666\">200</span>, <span style=\"color: #666666\">150</span>, <span style=\"color: #666666\">250</span>);\nconsole.dir(rectangle);\n\nrectangle.scale(<span style=\"color: #666666\">5</span>);\nconsole.dir(rectangle);\n\n<span style=\"color: #589632; font-weight: bold\">const</span> center <span style=\"color: #666666\">=</span> rectangle.center();\nconsole.dir(center);\n</pre>\n          </div>\n\n          <p>\n            <i\n              >Please note that the API surface is fairly minimal at the moment\n              and will be extended in the future.</i\n            >\n          </p>\n        </section>\n      </div>\n    </div>\n\n    <div id=\"layers-control\"></div>\n\n    <a\n      class=\"github-fork-ribbon\"\n      href=\"https://github.com/qgis/qgis-js\"\n      data-ribbon=\"Fork me on GitHub\"\n      title=\"Fork me on GitHub\"\n      >Fork me on GitHub</a\n    >\n\n    <script type=\"module\" src=\"src/index.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "sites/dev/package.json",
    "content": "{\n  \"name\": \"@qgis-js/dev\",\n  \"version\": \"4.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite dev\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\",\n    \"deploy\": \"npm run build && npm run deploy:upload\",\n    \"deploy:upload\": \"rsync --recursive --delete-before dist/ tux@zrhwpk.asuscomm.com:/data/https-portal/vhosts/qgis-js.dev.schmuki.io\"\n  },\n  \"dependencies\": {\n    \"@qgis-js/ol\": \"workspace:*\",\n    \"@qgis-js/utils\": \"workspace:*\",\n    \"qgis-js\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"coi-serviceworker\": \"0.1.7\",\n    \"ol\": \"^10.8.0\",\n    \"typescript\": \"5.8.2\",\n    \"vite\": \"8.0.0\",\n    \"vite-plugin-static-copy\": \"3.3.0\"\n  }\n}\n"
  },
  {
    "path": "sites/dev/public/projects/village/buildings.prj",
    "content": "PROJCS[\"OSGB_1936_British_National_Grid\",GEOGCS[\"GCS_OSGB 1936\",DATUM[\"D_OSGB_1936\",SPHEROID[\"Airy_1830\",6377563.396,299.3249646]],PRIMEM[\"Greenwich\",0],UNIT[\"Degree\",0.017453292519943295]],PROJECTION[\"Transverse_Mercator\"],PARAMETER[\"latitude_of_origin\",49],PARAMETER[\"central_meridian\",-2],PARAMETER[\"scale_factor\",0.9996012717],PARAMETER[\"false_easting\",400000],PARAMETER[\"false_northing\",-100000],UNIT[\"Meter\",1]]"
  },
  {
    "path": "sites/dev/public/projects/village/buildings.qpj",
    "content": "PROJCS[\"OSGB 1936 / British National Grid\",GEOGCS[\"OSGB 1936\",DATUM[\"OSGB_1936\",SPHEROID[\"Airy 1830\",6377563.396,299.3249646,AUTHORITY[\"EPSG\",\"7001\"]],TOWGS84[446.448,-125.157,542.06,0.15,0.247,0.842,-20.489],AUTHORITY[\"EPSG\",\"6277\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4277\"]],PROJECTION[\"Transverse_Mercator\"],PARAMETER[\"latitude_of_origin\",49],PARAMETER[\"central_meridian\",-2],PARAMETER[\"scale_factor\",0.9996012717],PARAMETER[\"false_easting\",400000],PARAMETER[\"false_northing\",-100000],UNIT[\"metre\",1,AUTHORITY[\"EPSG\",\"9001\"]],AXIS[\"Easting\",EAST],AXIS[\"Northing\",NORTH],AUTHORITY[\"EPSG\",\"27700\"]]\n"
  },
  {
    "path": "sites/dev/public/projects/village/project.qgs",
    "content": "<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>\n<qgis saveUserFull=\"Martin\" saveUser=\"martin\" saveDateTime=\"2022-03-17T23:03:52\" projectname=\"\" version=\"3.25.0-Master\">\n  <homePath path=\"\"/>\n  <title></title>\n  <autotransaction active=\"0\"/>\n  <evaluateDefaultValues active=\"0\"/>\n  <trust active=\"0\"/>\n  <projectCrs>\n    <spatialrefsys nativeFormat=\"Wkt\">\n      <wkt>PROJCRS[\"OSGB 1936 / British National Grid\",BASEGEOGCRS[\"OSGB 1936\",DATUM[\"OSGB 1936\",ELLIPSOID[\"Airy 1830\",6377563.396,299.3249646,LENGTHUNIT[\"metre\",1]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],ID[\"EPSG\",4277]],CONVERSION[\"British National Grid\",METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],PARAMETER[\"Latitude of natural origin\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",-2,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",0.9996012717,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",400000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",-100000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1]],USAGE[SCOPE[\"unknown\"],AREA[\"UK - Britain and UKCS 49°46'N to 61°01'N, 7°33'W to 3°33'E\"],BBOX[49.75,-9.2,61.14,2.88]],ID[\"EPSG\",27700]]</wkt>\n      <proj4>+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +units=m +no_defs</proj4>\n      <srsid>2437</srsid>\n      <srid>27700</srid>\n      <authid>EPSG:27700</authid>\n      <description>OSGB 1936 / British National Grid</description>\n      <projectionacronym>tmerc</projectionacronym>\n      <ellipsoidacronym>EPSG:7001</ellipsoidacronym>\n      <geographicflag>false</geographicflag>\n    </spatialrefsys>\n  </projectCrs>\n  <layer-tree-group>\n    <customproperties>\n      <Option/>\n    </customproperties>\n    <layer-tree-layer name=\"buildings\" source=\"./buildings.shp\" expanded=\"1\" legend_split_behavior=\"0\" legend_exp=\"\" patch_size=\"-1,-1\" id=\"buildings_c409011b_d787_426a_bb0e_ae5d79d6e4d7\" checked=\"Qt::Checked\" providerKey=\"ogr\">\n      <customproperties>\n        <Option/>\n      </customproperties>\n    </layer-tree-layer>\n    <layer-tree-layer name=\"rgb\" source=\"./rgb.tif\" expanded=\"1\" legend_split_behavior=\"0\" legend_exp=\"\" patch_size=\"-1,-1\" id=\"rgb_6623eb8c_6d94_452c_b9fa_efb9f472ca24\" checked=\"Qt::Checked\" providerKey=\"gdal\">\n      <customproperties>\n        <Option/>\n      </customproperties>\n    </layer-tree-layer>\n    <custom-order enabled=\"0\">\n      <item>rgb_6623eb8c_6d94_452c_b9fa_efb9f472ca24</item>\n      <item>buildings_c409011b_d787_426a_bb0e_ae5d79d6e4d7</item>\n    </custom-order>\n  </layer-tree-group>\n  <snapping-settings enabled=\"0\" tolerance=\"12\" mode=\"2\" type=\"0\" unit=\"1\" intersection-snapping=\"0\" maxScale=\"0\" self-snapping=\"0\" scaleDependencyMode=\"0\" minScale=\"0\">\n    <individual-layer-settings>\n      <layer-setting enabled=\"0\" tolerance=\"12\" type=\"0\" units=\"1\" id=\"buildings_c409011b_d787_426a_bb0e_ae5d79d6e4d7\" maxScale=\"0\" minScale=\"0\"/>\n    </individual-layer-settings>\n  </snapping-settings>\n  <relations/>\n  <polymorphicRelations/>\n  <mapcanvas name=\"theMapCanvas\" annotationsVisible=\"1\">\n    <units>meters</units>\n    <extent>\n      <xmin>321800.93915886041941121</xmin>\n      <ymin>129496.26018269738415256</ymin>\n      <xmax>321994.0257211935822852</xmax>\n      <ymax>129620.08884272881550714</ymax>\n    </extent>\n    <rotation>0</rotation>\n    <destinationsrs>\n      <spatialrefsys nativeFormat=\"Wkt\">\n        <wkt>PROJCRS[\"OSGB 1936 / British National Grid\",BASEGEOGCRS[\"OSGB 1936\",DATUM[\"OSGB 1936\",ELLIPSOID[\"Airy 1830\",6377563.396,299.3249646,LENGTHUNIT[\"metre\",1]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],ID[\"EPSG\",4277]],CONVERSION[\"British National Grid\",METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],PARAMETER[\"Latitude of natural origin\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",-2,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",0.9996012717,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",400000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",-100000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1]],USAGE[SCOPE[\"unknown\"],AREA[\"UK - Britain and UKCS 49°46'N to 61°01'N, 7°33'W to 3°33'E\"],BBOX[49.75,-9.2,61.14,2.88]],ID[\"EPSG\",27700]]</wkt>\n        <proj4>+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +units=m +no_defs</proj4>\n        <srsid>2437</srsid>\n        <srid>27700</srid>\n        <authid>EPSG:27700</authid>\n        <description>OSGB 1936 / British National Grid</description>\n        <projectionacronym>tmerc</projectionacronym>\n        <ellipsoidacronym>EPSG:7001</ellipsoidacronym>\n        <geographicflag>false</geographicflag>\n      </spatialrefsys>\n    </destinationsrs>\n    <rendermaptile>0</rendermaptile>\n    <expressionContextScope/>\n  </mapcanvas>\n  <projectModels/>\n  <legend updateDrawingOrder=\"true\">\n    <legendlayer name=\"buildings\" showFeatureCount=\"0\" checked=\"Qt::Checked\" drawingOrder=\"-1\" open=\"true\">\n      <filegroup hidden=\"false\" open=\"true\">\n        <legendlayerfile layerid=\"buildings_c409011b_d787_426a_bb0e_ae5d79d6e4d7\" visible=\"1\" isInOverview=\"0\"/>\n      </filegroup>\n    </legendlayer>\n    <legendlayer name=\"rgb\" showFeatureCount=\"0\" checked=\"Qt::Checked\" drawingOrder=\"-1\" open=\"true\">\n      <filegroup hidden=\"false\" open=\"true\">\n        <legendlayerfile layerid=\"rgb_6623eb8c_6d94_452c_b9fa_efb9f472ca24\" visible=\"1\" isInOverview=\"0\"/>\n      </filegroup>\n    </legendlayer>\n  </legend>\n  <mapViewDocks/>\n  <main-annotation-layer legendPlaceholderImage=\"\" type=\"annotation\" autoRefreshTime=\"0\" refreshOnNotifyEnabled=\"0\" autoRefreshEnabled=\"0\" refreshOnNotifyMessage=\"\">\n    <id>Annotations_5a901522_5198_4532_be03_e7edcd668017</id>\n    <datasource></datasource>\n    <keywordList>\n      <value></value>\n    </keywordList>\n    <layername>Annotations</layername>\n    <srs>\n      <spatialrefsys nativeFormat=\"Wkt\">\n        <wkt>GEOGCRS[\"WGS 84\",DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],CS[ellipsoidal,2],AXIS[\"geodetic latitude (Lat)\",north,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433]],AXIS[\"geodetic longitude (Lon)\",east,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433]],USAGE[SCOPE[\"unknown\"],AREA[\"World\"],BBOX[-90,-180,90,180]],ID[\"EPSG\",4326]]</wkt>\n        <proj4>+proj=longlat +datum=WGS84 +no_defs</proj4>\n        <srsid>3452</srsid>\n        <srid>4326</srid>\n        <authid>EPSG:4326</authid>\n        <description>WGS 84</description>\n        <projectionacronym>longlat</projectionacronym>\n        <ellipsoidacronym>EPSG:7030</ellipsoidacronym>\n        <geographicflag>true</geographicflag>\n      </spatialrefsys>\n    </srs>\n    <resourceMetadata>\n      <identifier></identifier>\n      <parentidentifier></parentidentifier>\n      <language></language>\n      <type></type>\n      <title></title>\n      <abstract></abstract>\n      <links/>\n      <fees></fees>\n      <encoding></encoding>\n      <crs>\n        <spatialrefsys nativeFormat=\"Wkt\">\n          <wkt></wkt>\n          <proj4></proj4>\n          <srsid>0</srsid>\n          <srid>0</srid>\n          <authid></authid>\n          <description></description>\n          <projectionacronym></projectionacronym>\n          <ellipsoidacronym></ellipsoidacronym>\n          <geographicflag>false</geographicflag>\n        </spatialrefsys>\n      </crs>\n      <extent/>\n    </resourceMetadata>\n    <items/>\n    <layerOpacity>1</layerOpacity>\n    <blendMode>0</blendMode>\n    <paintEffect/>\n  </main-annotation-layer>\n  <projectlayers>\n    <maplayer simplifyDrawingTol=\"1\" simplifyMaxScale=\"1\" simplifyLocal=\"1\" labelsEnabled=\"0\" minScale=\"100000000\" geometry=\"Polygon\" simplifyAlgorithm=\"0\" autoRefreshEnabled=\"0\" autoRefreshTime=\"0\" refreshOnNotifyEnabled=\"0\" type=\"vector\" maxScale=\"0\" legendPlaceholderImage=\"\" readOnly=\"0\" styleCategories=\"AllStyleCategories\" refreshOnNotifyMessage=\"\" simplifyDrawingHints=\"1\" hasScaleBasedVisibilityFlag=\"0\" symbologyReferenceScale=\"-1\" wkbType=\"MultiPolygon\">\n      <extent>\n        <xmin>320989.54106238950043917</xmin>\n        <ymin>128983.21402802964439616</ymin>\n        <xmax>322769.35696973575977609</xmax>\n        <ymax>130817.19739320731605403</ymax>\n      </extent>\n      <wgs84extent>\n        <xmin>-3.1290883526975195</xmin>\n        <ymin>51.05473100531580855</ymin>\n        <xmax>-3.10329775497044835</xmax>\n        <ymax>51.07146158335104502</ymax>\n      </wgs84extent>\n      <id>buildings_c409011b_d787_426a_bb0e_ae5d79d6e4d7</id>\n      <datasource>./buildings.shp</datasource>\n      <keywordList>\n        <value></value>\n      </keywordList>\n      <layername>buildings</layername>\n      <srs>\n        <spatialrefsys nativeFormat=\"Wkt\">\n          <wkt>PROJCRS[\"OSGB 1936 / British National Grid\",BASEGEOGCRS[\"OSGB 1936\",DATUM[\"OSGB 1936\",ELLIPSOID[\"Airy 1830\",6377563.396,299.3249646,LENGTHUNIT[\"metre\",1]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],ID[\"EPSG\",4277]],CONVERSION[\"British National Grid\",METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],PARAMETER[\"Latitude of natural origin\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",-2,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",0.9996012717,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",400000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",-100000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1]],USAGE[SCOPE[\"unknown\"],AREA[\"UK - Britain and UKCS 49°46'N to 61°01'N, 7°33'W to 3°33'E\"],BBOX[49.75,-9.2,61.14,2.88]],ID[\"EPSG\",27700]]</wkt>\n          <proj4>+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +units=m +no_defs</proj4>\n          <srsid>2437</srsid>\n          <srid>27700</srid>\n          <authid>EPSG:27700</authid>\n          <description>OSGB 1936 / British National Grid</description>\n          <projectionacronym>tmerc</projectionacronym>\n          <ellipsoidacronym>EPSG:7001</ellipsoidacronym>\n          <geographicflag>false</geographicflag>\n        </spatialrefsys>\n      </srs>\n      <resourceMetadata>\n        <identifier></identifier>\n        <parentidentifier></parentidentifier>\n        <language></language>\n        <type>dataset</type>\n        <title></title>\n        <abstract></abstract>\n        <links/>\n        <fees></fees>\n        <encoding></encoding>\n        <crs>\n          <spatialrefsys nativeFormat=\"Wkt\">\n            <wkt></wkt>\n            <proj4></proj4>\n            <srsid>0</srsid>\n            <srid>0</srid>\n            <authid></authid>\n            <description></description>\n            <projectionacronym></projectionacronym>\n            <ellipsoidacronym></ellipsoidacronym>\n            <geographicflag>false</geographicflag>\n          </spatialrefsys>\n        </crs>\n        <extent/>\n      </resourceMetadata>\n      <provider encoding=\"UTF-8\">ogr</provider>\n      <vectorjoins/>\n      <layerDependencies/>\n      <dataDependencies/>\n      <expressionfields/>\n      <map-layer-style-manager current=\"default\">\n        <map-layer-style name=\"default\"/>\n      </map-layer-style-manager>\n      <auxiliaryLayer/>\n      <metadataUrls/>\n      <flags>\n        <Identifiable>1</Identifiable>\n        <Removable>1</Removable>\n        <Searchable>1</Searchable>\n        <Private>0</Private>\n      </flags>\n      <temporal enabled=\"0\" durationField=\"\" startExpression=\"\" startField=\"\" mode=\"0\" limitMode=\"0\" accumulate=\"0\" durationUnit=\"min\" endField=\"\" endExpression=\"\" fixedDuration=\"0\">\n        <fixedRange>\n          <start></start>\n          <end></end>\n        </fixedRange>\n      </temporal>\n      <elevation extrusionEnabled=\"0\" zscale=\"1\" binding=\"Centroid\" extrusion=\"0\" clamping=\"Terrain\" zoffset=\"0\"/>\n      <renderer-v2 enableorderby=\"0\" type=\"singleSymbol\" forceraster=\"0\" symbollevels=\"0\" referencescale=\"-1\">\n        <symbols>\n          <symbol name=\"0\" force_rhr=\"0\" type=\"fill\" clip_to_extent=\"1\" alpha=\"1\">\n            <data_defined_properties>\n              <Option type=\"Map\">\n                <Option name=\"name\" type=\"QString\" value=\"\"/>\n                <Option name=\"properties\"/>\n                <Option name=\"type\" type=\"QString\" value=\"collection\"/>\n              </Option>\n            </data_defined_properties>\n            <layer enabled=\"1\" pass=\"0\" class=\"GradientFill\" locked=\"0\">\n              <Option type=\"Map\">\n                <Option name=\"angle\" type=\"QString\" value=\"45\"/>\n                <Option name=\"color\" type=\"QString\" value=\"0,0,0,255\"/>\n                <Option name=\"color1\" type=\"QString\" value=\"247,251,255,255\"/>\n                <Option name=\"color2\" type=\"QString\" value=\"8,48,107,255\"/>\n                <Option name=\"color_type\" type=\"QString\" value=\"1\"/>\n                <Option name=\"coordinate_mode\" type=\"QString\" value=\"0\"/>\n                <Option name=\"direction\" type=\"QString\" value=\"ccw\"/>\n                <Option name=\"discrete\" type=\"QString\" value=\"0\"/>\n                <Option name=\"gradient_color2\" type=\"QString\" value=\"255,255,255,255\"/>\n                <Option name=\"offset\" type=\"QString\" value=\"0,0\"/>\n                <Option name=\"offset_map_unit_scale\" type=\"QString\" value=\"3x:0,0,0,0,0,0\"/>\n                <Option name=\"offset_unit\" type=\"QString\" value=\"MM\"/>\n                <Option name=\"rampType\" type=\"QString\" value=\"gradient\"/>\n                <Option name=\"reference_point1\" type=\"QString\" value=\"0.5,0\"/>\n                <Option name=\"reference_point1_iscentroid\" type=\"QString\" value=\"0\"/>\n                <Option name=\"reference_point2\" type=\"QString\" value=\"0.5,1\"/>\n                <Option name=\"reference_point2_iscentroid\" type=\"QString\" value=\"0\"/>\n                <Option name=\"spec\" type=\"QString\" value=\"rgb\"/>\n                <Option name=\"spread\" type=\"QString\" value=\"0\"/>\n                <Option name=\"stops\" type=\"QString\" value=\"0.13;222,235,247,255;rgb;ccw:0.26;198,219,239,255;rgb;ccw:0.39;158,202,225,255;rgb;ccw:0.52;107,174,214,255;rgb;ccw:0.65;66,146,198,255;rgb;ccw:0.78;33,113,181,255;rgb;ccw:0.9;8,81,156,255;rgb;ccw\"/>\n                <Option name=\"type\" type=\"QString\" value=\"0\"/>\n              </Option>\n              <prop k=\"angle\" v=\"45\"/>\n              <prop k=\"color\" v=\"0,0,0,255\"/>\n              <prop k=\"color1\" v=\"247,251,255,255\"/>\n              <prop k=\"color2\" v=\"8,48,107,255\"/>\n              <prop k=\"color_type\" v=\"1\"/>\n              <prop k=\"coordinate_mode\" v=\"0\"/>\n              <prop k=\"direction\" v=\"ccw\"/>\n              <prop k=\"discrete\" v=\"0\"/>\n              <prop k=\"gradient_color2\" v=\"255,255,255,255\"/>\n              <prop k=\"offset\" v=\"0,0\"/>\n              <prop k=\"offset_map_unit_scale\" v=\"3x:0,0,0,0,0,0\"/>\n              <prop k=\"offset_unit\" v=\"MM\"/>\n              <prop k=\"rampType\" v=\"gradient\"/>\n              <prop k=\"reference_point1\" v=\"0.5,0\"/>\n              <prop k=\"reference_point1_iscentroid\" v=\"0\"/>\n              <prop k=\"reference_point2\" v=\"0.5,1\"/>\n              <prop k=\"reference_point2_iscentroid\" v=\"0\"/>\n              <prop k=\"spec\" v=\"rgb\"/>\n              <prop k=\"spread\" v=\"0\"/>\n              <prop k=\"stops\" v=\"0.13;222,235,247,255;rgb;ccw:0.26;198,219,239,255;rgb;ccw:0.39;158,202,225,255;rgb;ccw:0.52;107,174,214,255;rgb;ccw:0.65;66,146,198,255;rgb;ccw:0.78;33,113,181,255;rgb;ccw:0.9;8,81,156,255;rgb;ccw\"/>\n              <prop k=\"type\" v=\"0\"/>\n              <data_defined_properties>\n                <Option type=\"Map\">\n                  <Option name=\"name\" type=\"QString\" value=\"\"/>\n                  <Option name=\"properties\"/>\n                  <Option name=\"type\" type=\"QString\" value=\"collection\"/>\n                </Option>\n              </data_defined_properties>\n            </layer>\n            <layer enabled=\"1\" pass=\"0\" class=\"SimpleLine\" locked=\"0\">\n              <Option type=\"Map\">\n                <Option name=\"align_dash_pattern\" type=\"QString\" value=\"0\"/>\n                <Option name=\"capstyle\" type=\"QString\" value=\"square\"/>\n                <Option name=\"customdash\" type=\"QString\" value=\"5;2\"/>\n                <Option name=\"customdash_map_unit_scale\" type=\"QString\" value=\"3x:0,0,0,0,0,0\"/>\n                <Option name=\"customdash_unit\" type=\"QString\" value=\"MM\"/>\n                <Option name=\"dash_pattern_offset\" type=\"QString\" value=\"0\"/>\n                <Option name=\"dash_pattern_offset_map_unit_scale\" type=\"QString\" value=\"3x:0,0,0,0,0,0\"/>\n                <Option name=\"dash_pattern_offset_unit\" type=\"QString\" value=\"MM\"/>\n                <Option name=\"draw_inside_polygon\" type=\"QString\" value=\"0\"/>\n                <Option name=\"joinstyle\" type=\"QString\" value=\"bevel\"/>\n                <Option name=\"line_color\" type=\"QString\" value=\"96,91,91,255\"/>\n                <Option name=\"line_style\" type=\"QString\" value=\"solid\"/>\n                <Option name=\"line_width\" type=\"QString\" value=\"0.6\"/>\n                <Option name=\"line_width_unit\" type=\"QString\" value=\"MM\"/>\n                <Option name=\"offset\" type=\"QString\" value=\"0\"/>\n                <Option name=\"offset_map_unit_scale\" type=\"QString\" value=\"3x:0,0,0,0,0,0\"/>\n                <Option name=\"offset_unit\" type=\"QString\" value=\"MM\"/>\n                <Option name=\"ring_filter\" type=\"QString\" value=\"0\"/>\n                <Option name=\"trim_distance_end\" type=\"QString\" value=\"0\"/>\n                <Option name=\"trim_distance_end_map_unit_scale\" type=\"QString\" value=\"3x:0,0,0,0,0,0\"/>\n                <Option name=\"trim_distance_end_unit\" type=\"QString\" value=\"MM\"/>\n                <Option name=\"trim_distance_start\" type=\"QString\" value=\"0\"/>\n                <Option name=\"trim_distance_start_map_unit_scale\" type=\"QString\" value=\"3x:0,0,0,0,0,0\"/>\n                <Option name=\"trim_distance_start_unit\" type=\"QString\" value=\"MM\"/>\n                <Option name=\"tweak_dash_pattern_on_corners\" type=\"QString\" value=\"0\"/>\n                <Option name=\"use_custom_dash\" type=\"QString\" value=\"0\"/>\n                <Option name=\"width_map_unit_scale\" type=\"QString\" value=\"3x:0,0,0,0,0,0\"/>\n              </Option>\n              <prop k=\"align_dash_pattern\" v=\"0\"/>\n              <prop k=\"capstyle\" v=\"square\"/>\n              <prop k=\"customdash\" v=\"5;2\"/>\n              <prop k=\"customdash_map_unit_scale\" v=\"3x:0,0,0,0,0,0\"/>\n              <prop k=\"customdash_unit\" v=\"MM\"/>\n              <prop k=\"dash_pattern_offset\" v=\"0\"/>\n              <prop k=\"dash_pattern_offset_map_unit_scale\" v=\"3x:0,0,0,0,0,0\"/>\n              <prop k=\"dash_pattern_offset_unit\" v=\"MM\"/>\n              <prop k=\"draw_inside_polygon\" v=\"0\"/>\n              <prop k=\"joinstyle\" v=\"bevel\"/>\n              <prop k=\"line_color\" v=\"96,91,91,255\"/>\n              <prop k=\"line_style\" v=\"solid\"/>\n              <prop k=\"line_width\" v=\"0.6\"/>\n              <prop k=\"line_width_unit\" v=\"MM\"/>\n              <prop k=\"offset\" v=\"0\"/>\n              <prop k=\"offset_map_unit_scale\" v=\"3x:0,0,0,0,0,0\"/>\n              <prop k=\"offset_unit\" v=\"MM\"/>\n              <prop k=\"ring_filter\" v=\"0\"/>\n              <prop k=\"trim_distance_end\" v=\"0\"/>\n              <prop k=\"trim_distance_end_map_unit_scale\" v=\"3x:0,0,0,0,0,0\"/>\n              <prop k=\"trim_distance_end_unit\" v=\"MM\"/>\n              <prop k=\"trim_distance_start\" v=\"0\"/>\n              <prop k=\"trim_distance_start_map_unit_scale\" v=\"3x:0,0,0,0,0,0\"/>\n              <prop k=\"trim_distance_start_unit\" v=\"MM\"/>\n              <prop k=\"tweak_dash_pattern_on_corners\" v=\"0\"/>\n              <prop k=\"use_custom_dash\" v=\"0\"/>\n              <prop k=\"width_map_unit_scale\" v=\"3x:0,0,0,0,0,0\"/>\n              <data_defined_properties>\n                <Option type=\"Map\">\n                  <Option name=\"name\" type=\"QString\" value=\"\"/>\n                  <Option name=\"properties\"/>\n                  <Option name=\"type\" type=\"QString\" value=\"collection\"/>\n                </Option>\n              </data_defined_properties>\n            </layer>\n          </symbol>\n        </symbols>\n        <rotation/>\n        <sizescale/>\n      </renderer-v2>\n      <customproperties>\n        <Option/>\n      </customproperties>\n      <blendMode>0</blendMode>\n      <featureBlendMode>0</featureBlendMode>\n      <layerOpacity>1</layerOpacity>\n      <geometryOptions removeDuplicateNodes=\"0\" geometryPrecision=\"0\">\n        <activeChecks type=\"StringList\">\n          <Option type=\"QString\" value=\"\"/>\n        </activeChecks>\n        <checkConfiguration/>\n      </geometryOptions>\n      <legend type=\"default-vector\" showLabelLegend=\"0\"/>\n      <referencedLayers/>\n      <fieldConfiguration>\n        <field name=\"ogc_fid\" configurationFlags=\"None\">\n          <editWidget type=\"\">\n            <config>\n              <Option/>\n            </config>\n          </editWidget>\n        </field>\n      </fieldConfiguration>\n      <aliases>\n        <alias index=\"0\" name=\"\" field=\"ogc_fid\"/>\n      </aliases>\n      <defaults>\n        <default field=\"ogc_fid\" expression=\"\" applyOnUpdate=\"0\"/>\n      </defaults>\n      <constraints>\n        <constraint field=\"ogc_fid\" notnull_strength=\"0\" constraints=\"0\" unique_strength=\"0\" exp_strength=\"0\"/>\n      </constraints>\n      <constraintExpressions>\n        <constraint desc=\"\" field=\"ogc_fid\" exp=\"\"/>\n      </constraintExpressions>\n      <expressionfields/>\n      <attributeactions>\n        <defaultAction key=\"Canvas\" value=\"{00000000-0000-0000-0000-000000000000}\"/>\n      </attributeactions>\n      <attributetableconfig actionWidgetStyle=\"dropDown\" sortOrder=\"0\" sortExpression=\"\">\n        <columns/>\n      </attributetableconfig>\n      <conditionalstyles>\n        <rowstyles/>\n        <fieldstyles/>\n      </conditionalstyles>\n      <storedexpressions/>\n      <editform tolerant=\"1\"></editform>\n      <editforminit/>\n      <editforminitcodesource>0</editforminitcodesource>\n      <editforminitfilepath></editforminitfilepath>\n      <editforminitcode><![CDATA[]]></editforminitcode>\n      <featformsuppress>0</featformsuppress>\n      <editorlayout>generatedlayout</editorlayout>\n      <editable/>\n      <labelOnTop/>\n      <reuseLastValue/>\n      <dataDefinedFieldProperties/>\n      <widgets/>\n      <previewExpression></previewExpression>\n      <mapTip></mapTip>\n    </maplayer>\n    <maplayer legendPlaceholderImage=\"\" type=\"raster\" autoRefreshTime=\"0\" maxScale=\"0\" refreshOnNotifyEnabled=\"0\" autoRefreshEnabled=\"0\" styleCategories=\"AllStyleCategories\" hasScaleBasedVisibilityFlag=\"0\" minScale=\"1e+08\" refreshOnNotifyMessage=\"\">\n      <extent>\n        <xmin>321000</xmin>\n        <ymin>129000.94442700000945479</ymin>\n        <xmax>322800</xmax>\n        <ymax>130800.94442700000945479</ymax>\n      </extent>\n      <wgs84extent>\n        <xmin>-3.12893554519487038</xmin>\n        <ymin>51.05489184983709805</ymin>\n        <xmax>-3.1028644136382888</xmax>\n        <ymax>51.07131958612462341</ymax>\n      </wgs84extent>\n      <id>rgb_6623eb8c_6d94_452c_b9fa_efb9f472ca24</id>\n      <datasource>./rgb.tif</datasource>\n      <keywordList>\n        <value></value>\n      </keywordList>\n      <layername>rgb</layername>\n      <srs>\n        <spatialrefsys nativeFormat=\"Wkt\">\n          <wkt>PROJCRS[\"OSGB 1936 / British National Grid\",BASEGEOGCRS[\"OSGB 1936\",DATUM[\"OSGB 1936\",ELLIPSOID[\"Airy 1830\",6377563.396,299.3249646,LENGTHUNIT[\"metre\",1]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],ID[\"EPSG\",4277]],CONVERSION[\"British National Grid\",METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],PARAMETER[\"Latitude of natural origin\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",-2,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",0.9996012717,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",400000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",-100000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1]],USAGE[SCOPE[\"unknown\"],AREA[\"UK - Britain and UKCS 49°46'N to 61°01'N, 7°33'W to 3°33'E\"],BBOX[49.75,-9.2,61.14,2.88]],ID[\"EPSG\",27700]]</wkt>\n          <proj4>+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +units=m +no_defs</proj4>\n          <srsid>2437</srsid>\n          <srid>27700</srid>\n          <authid>EPSG:27700</authid>\n          <description>OSGB 1936 / British National Grid</description>\n          <projectionacronym>tmerc</projectionacronym>\n          <ellipsoidacronym>EPSG:7001</ellipsoidacronym>\n          <geographicflag>false</geographicflag>\n        </spatialrefsys>\n      </srs>\n      <resourceMetadata>\n        <identifier></identifier>\n        <parentidentifier></parentidentifier>\n        <language></language>\n        <type></type>\n        <title></title>\n        <abstract></abstract>\n        <contact>\n          <name></name>\n          <organization></organization>\n          <position></position>\n          <voice></voice>\n          <fax></fax>\n          <email></email>\n          <role></role>\n        </contact>\n        <links/>\n        <fees></fees>\n        <encoding></encoding>\n        <crs>\n          <spatialrefsys nativeFormat=\"Wkt\">\n            <wkt></wkt>\n            <proj4></proj4>\n            <srsid>0</srsid>\n            <srid>0</srid>\n            <authid></authid>\n            <description></description>\n            <projectionacronym></projectionacronym>\n            <ellipsoidacronym></ellipsoidacronym>\n            <geographicflag>false</geographicflag>\n          </spatialrefsys>\n        </crs>\n        <extent>\n          <spatial maxx=\"0\" miny=\"0\" minx=\"0\" dimensions=\"2\" minz=\"0\" maxy=\"0\" maxz=\"0\" crs=\"\"/>\n          <temporal>\n            <period>\n              <start></start>\n              <end></end>\n            </period>\n          </temporal>\n        </extent>\n      </resourceMetadata>\n      <provider>gdal</provider>\n      <noData>\n        <noDataList useSrcNoData=\"0\" bandNo=\"1\"/>\n        <noDataList useSrcNoData=\"0\" bandNo=\"2\"/>\n        <noDataList useSrcNoData=\"0\" bandNo=\"3\"/>\n      </noData>\n      <map-layer-style-manager current=\"default\">\n        <map-layer-style name=\"default\"/>\n      </map-layer-style-manager>\n      <metadataUrls/>\n      <flags>\n        <Identifiable>1</Identifiable>\n        <Removable>1</Removable>\n        <Searchable>0</Searchable>\n        <Private>0</Private>\n      </flags>\n      <temporal enabled=\"0\" mode=\"0\" fetchMode=\"0\">\n        <fixedRange>\n          <start></start>\n          <end></end>\n        </fixedRange>\n      </temporal>\n      <elevation enabled=\"0\" zscale=\"1\" zoffset=\"0\"/>\n      <customproperties>\n        <Option type=\"Map\">\n          <Option name=\"WMSBackgroundLayer\" type=\"bool\" value=\"false\"/>\n          <Option name=\"WMSPublishDataSourceUrl\" type=\"bool\" value=\"false\"/>\n          <Option name=\"embeddedWidgets/count\" type=\"int\" value=\"0\"/>\n          <Option name=\"identify/format\" type=\"QString\" value=\"Value\"/>\n        </Option>\n      </customproperties>\n      <pipe-data-defined-properties>\n        <Option type=\"Map\">\n          <Option name=\"name\" type=\"QString\" value=\"\"/>\n          <Option name=\"properties\"/>\n          <Option name=\"type\" type=\"QString\" value=\"collection\"/>\n        </Option>\n      </pipe-data-defined-properties>\n      <pipe>\n        <provider>\n          <resampling enabled=\"false\" zoomedInResamplingMethod=\"nearestNeighbour\" zoomedOutResamplingMethod=\"nearestNeighbour\" maxOversampling=\"2\"/>\n        </provider>\n        <rasterrenderer redBand=\"1\" type=\"multibandcolor\" opacity=\"1\" blueBand=\"3\" alphaBand=\"-1\" nodataColor=\"\" greenBand=\"2\">\n          <rasterTransparency/>\n          <minMaxOrigin>\n            <limits>MinMax</limits>\n            <extent>WholeRaster</extent>\n            <statAccuracy>Estimated</statAccuracy>\n            <cumulativeCutLower>0.02</cumulativeCutLower>\n            <cumulativeCutUpper>0.98</cumulativeCutUpper>\n            <stdDevFactor>2</stdDevFactor>\n          </minMaxOrigin>\n        </rasterrenderer>\n        <brightnesscontrast brightness=\"0\" gamma=\"1\" contrast=\"0\"/>\n        <huesaturation colorizeOn=\"0\" colorizeGreen=\"128\" invertColors=\"0\" saturation=\"0\" colorizeBlue=\"128\" colorizeRed=\"255\" colorizeStrength=\"100\" grayscaleMode=\"0\"/>\n        <rasterresampler maxOversampling=\"2\"/>\n        <resamplingStage>resamplingFilter</resamplingStage>\n      </pipe>\n      <blendMode>0</blendMode>\n    </maplayer>\n  </projectlayers>\n  <layerorder>\n    <layer id=\"rgb_6623eb8c_6d94_452c_b9fa_efb9f472ca24\"/>\n    <layer id=\"buildings_c409011b_d787_426a_bb0e_ae5d79d6e4d7\"/>\n  </layerorder>\n  <properties>\n    <DefaultStyles>\n      <ColorRamp type=\"QString\"></ColorRamp>\n      <Fill type=\"QString\"></Fill>\n      <Line type=\"QString\"></Line>\n      <Marker type=\"QString\"></Marker>\n      <Opacity type=\"double\">1</Opacity>\n      <RandomColors type=\"bool\">true</RandomColors>\n    </DefaultStyles>\n    <Digitizing>\n      <AvoidIntersectionsMode type=\"int\">0</AvoidIntersectionsMode>\n    </Digitizing>\n    <Gui>\n      <CanvasColorBluePart type=\"int\">255</CanvasColorBluePart>\n      <CanvasColorGreenPart type=\"int\">255</CanvasColorGreenPart>\n      <CanvasColorRedPart type=\"int\">255</CanvasColorRedPart>\n      <SelectionColorAlphaPart type=\"int\">255</SelectionColorAlphaPart>\n      <SelectionColorBluePart type=\"int\">0</SelectionColorBluePart>\n      <SelectionColorGreenPart type=\"int\">255</SelectionColorGreenPart>\n      <SelectionColorRedPart type=\"int\">255</SelectionColorRedPart>\n    </Gui>\n    <Legend>\n      <filterByMap type=\"bool\">false</filterByMap>\n    </Legend>\n    <Macros>\n      <pythonCode type=\"QString\"></pythonCode>\n    </Macros>\n    <Measure>\n      <Ellipsoid type=\"QString\">EPSG:7001</Ellipsoid>\n    </Measure>\n    <Measurement>\n      <AreaUnits type=\"QString\">m2</AreaUnits>\n      <DistanceUnits type=\"QString\">meters</DistanceUnits>\n    </Measurement>\n    <PAL>\n      <CandidatesLinePerCM type=\"double\">5</CandidatesLinePerCM>\n      <CandidatesPolygonPerCM type=\"double\">2.5</CandidatesPolygonPerCM>\n      <DrawRectOnly type=\"bool\">false</DrawRectOnly>\n      <DrawUnplaced type=\"bool\">false</DrawUnplaced>\n      <PlacementEngineVersion type=\"int\">1</PlacementEngineVersion>\n      <SearchMethod type=\"int\">0</SearchMethod>\n      <ShowingAllLabels type=\"bool\">false</ShowingAllLabels>\n      <ShowingCandidates type=\"bool\">false</ShowingCandidates>\n      <ShowingPartialsLabels type=\"bool\">true</ShowingPartialsLabels>\n      <TextFormat type=\"int\">0</TextFormat>\n      <UnplacedColor type=\"QString\">255,0,0,255</UnplacedColor>\n    </PAL>\n    <Paths>\n      <Absolute type=\"bool\">false</Absolute>\n    </Paths>\n    <PositionPrecision>\n      <Automatic type=\"bool\">true</Automatic>\n      <DecimalPlaces type=\"int\">2</DecimalPlaces>\n      <DegreeFormat type=\"QString\">MU</DegreeFormat>\n    </PositionPrecision>\n    <RenderMapTile type=\"bool\">false</RenderMapTile>\n    <SpatialRefSys>\n      <ProjectionsEnabled type=\"int\">1</ProjectionsEnabled>\n    </SpatialRefSys>\n    <WCSLayers type=\"QStringList\"/>\n    <WCSUrl type=\"QString\"></WCSUrl>\n    <WFSLayers type=\"QStringList\"/>\n    <WFSTLayers>\n      <Delete type=\"QStringList\"/>\n      <Insert type=\"QStringList\"/>\n      <Update type=\"QStringList\"/>\n    </WFSTLayers>\n    <WFSUrl type=\"QString\"></WFSUrl>\n    <WMSAccessConstraints type=\"QString\">None</WMSAccessConstraints>\n    <WMSAddWktGeometry type=\"bool\">false</WMSAddWktGeometry>\n    <WMSContactMail type=\"QString\"></WMSContactMail>\n    <WMSContactOrganization type=\"QString\"></WMSContactOrganization>\n    <WMSContactPerson type=\"QString\"></WMSContactPerson>\n    <WMSContactPhone type=\"QString\"></WMSContactPhone>\n    <WMSContactPosition type=\"QString\"></WMSContactPosition>\n    <WMSDefaultMapUnitsPerMm type=\"double\">1</WMSDefaultMapUnitsPerMm>\n    <WMSFeatureInfoUseAttributeFormSettings type=\"bool\">false</WMSFeatureInfoUseAttributeFormSettings>\n    <WMSFees type=\"QString\">conditions unknown</WMSFees>\n    <WMSImageQuality type=\"int\">90</WMSImageQuality>\n    <WMSKeywordList type=\"QStringList\">\n      <value></value>\n    </WMSKeywordList>\n    <WMSMaxAtlasFeatures type=\"int\">1</WMSMaxAtlasFeatures>\n    <WMSOnlineResource type=\"QString\"></WMSOnlineResource>\n    <WMSPrecision type=\"QString\">8</WMSPrecision>\n    <WMSRootName type=\"QString\"></WMSRootName>\n    <WMSSegmentizeFeatureInfoGeometry type=\"bool\">false</WMSSegmentizeFeatureInfoGeometry>\n    <WMSServiceAbstract type=\"QString\"></WMSServiceAbstract>\n    <WMSServiceCapabilities type=\"bool\">false</WMSServiceCapabilities>\n    <WMSServiceTitle type=\"QString\"></WMSServiceTitle>\n    <WMSTileBuffer type=\"int\">0</WMSTileBuffer>\n    <WMSUrl type=\"QString\"></WMSUrl>\n    <WMSUseLayerIDs type=\"bool\">false</WMSUseLayerIDs>\n    <WMTSGrids>\n      <CRS type=\"QStringList\"/>\n      <Config type=\"QStringList\"/>\n    </WMTSGrids>\n    <WMTSJpegLayers>\n      <Group type=\"QStringList\"/>\n      <Layer type=\"QStringList\"/>\n      <Project type=\"bool\">false</Project>\n    </WMTSJpegLayers>\n    <WMTSLayers>\n      <Group type=\"QStringList\"/>\n      <Layer type=\"QStringList\"/>\n      <Project type=\"bool\">false</Project>\n    </WMTSLayers>\n    <WMTSMinScale type=\"int\">5000</WMTSMinScale>\n    <WMTSPngLayers>\n      <Group type=\"QStringList\"/>\n      <Layer type=\"QStringList\"/>\n      <Project type=\"bool\">false</Project>\n    </WMTSPngLayers>\n    <WMTSUrl type=\"QString\"></WMTSUrl>\n    <ddt2>\n      <designs type=\"QString\"></designs>\n    </ddt2>\n  </properties>\n  <dataDefinedServerProperties>\n    <Option type=\"Map\">\n      <Option name=\"name\" type=\"QString\" value=\"\"/>\n      <Option name=\"properties\"/>\n      <Option name=\"type\" type=\"QString\" value=\"collection\"/>\n    </Option>\n  </dataDefinedServerProperties>\n  <visibility-presets/>\n  <transformContext/>\n  <projectMetadata>\n    <identifier></identifier>\n    <parentidentifier></parentidentifier>\n    <language></language>\n    <type></type>\n    <title></title>\n    <abstract></abstract>\n    <contact>\n      <name></name>\n      <organization></organization>\n      <position></position>\n      <voice></voice>\n      <fax></fax>\n      <email></email>\n      <role></role>\n    </contact>\n    <links/>\n    <author>Martin</author>\n    <creation>2022-03-08T23:01:27</creation>\n  </projectMetadata>\n  <Annotations/>\n  <Layouts/>\n  <mapViewDocks3D/>\n  <Bookmarks/>\n  <ProjectViewSettings UseProjectScales=\"0\">\n    <Scales/>\n    <DefaultViewExtent xmin=\"321800.93915886041941121\" ymin=\"129476.13437019710545428\" xmax=\"321994.0257211935822852\" ymax=\"129640.21465522909420542\">\n      <spatialrefsys nativeFormat=\"Wkt\">\n        <wkt>PROJCRS[\"OSGB 1936 / British National Grid\",BASEGEOGCRS[\"OSGB 1936\",DATUM[\"OSGB 1936\",ELLIPSOID[\"Airy 1830\",6377563.396,299.3249646,LENGTHUNIT[\"metre\",1]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],ID[\"EPSG\",4277]],CONVERSION[\"British National Grid\",METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],PARAMETER[\"Latitude of natural origin\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",-2,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",0.9996012717,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",400000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",-100000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1]],USAGE[SCOPE[\"unknown\"],AREA[\"UK - Britain and UKCS 49°46'N to 61°01'N, 7°33'W to 3°33'E\"],BBOX[49.75,-9.2,61.14,2.88]],ID[\"EPSG\",27700]]</wkt>\n        <proj4>+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +units=m +no_defs</proj4>\n        <srsid>2437</srsid>\n        <srid>27700</srid>\n        <authid>EPSG:27700</authid>\n        <description>OSGB 1936 / British National Grid</description>\n        <projectionacronym>tmerc</projectionacronym>\n        <ellipsoidacronym>EPSG:7001</ellipsoidacronym>\n        <geographicflag>false</geographicflag>\n      </spatialrefsys>\n    </DefaultViewExtent>\n  </ProjectViewSettings>\n  <ProjectTimeSettings timeStepUnit=\"h\" cumulativeTemporalRange=\"0\" frameRate=\"1\" timeStep=\"1\"/>\n  <ProjectDisplaySettings>\n    <BearingFormat id=\"bearing\">\n      <Option type=\"Map\">\n        <Option name=\"decimal_separator\" type=\"QChar\" value=\"\"/>\n        <Option name=\"decimals\" type=\"int\" value=\"6\"/>\n        <Option name=\"direction_format\" type=\"int\" value=\"0\"/>\n        <Option name=\"rounding_type\" type=\"int\" value=\"0\"/>\n        <Option name=\"show_plus\" type=\"bool\" value=\"false\"/>\n        <Option name=\"show_thousand_separator\" type=\"bool\" value=\"true\"/>\n        <Option name=\"show_trailing_zeros\" type=\"bool\" value=\"false\"/>\n        <Option name=\"thousand_separator\" type=\"QChar\" value=\"\"/>\n      </Option>\n    </BearingFormat>\n  </ProjectDisplaySettings>\n</qgis>\n"
  },
  {
    "path": "sites/dev/src/demo.css",
    "content": "body {\n  font:\n    16px/1.5em \"Overpass\",\n    \"Open Sans\",\n    Helvetica,\n    sans-serif;\n  color: #333;\n}\n\nh1 {\n  color: #589632;\n}\n\na {\n  color: #589632;\n  font-weight: bold;\n}\n\na.source {\n  font-family: \"Courier New\", Courier, monospace;\n  font-weight: normal;\n}\n\n.demo {\n  position: relative;\n  width: 100%;\n  height: 30em;\n  margin: 2em 0;\n  border: 1px solid #ccc;\n  background-color: #ffffff;\n  resize: both;\n}\n\n@keyframes loading {\n  0%,\n  100% {\n    transform: scaleX(0);\n  }\n  50% {\n    transform: scaleX(1);\n  }\n}\n\n.demo::before,\n.demo.spinner::before {\n  content: \"\";\n  transition: opacity 0.5s ease-in-out;\n}\n\n.demo::before {\n  opacity: 0;\n}\n\n.demo.spinner::before {\n  opacity: 1;\n\n  display: block;\n  position: absolute;\n  bottom: -0.5em;\n  left: 0.5em;\n  right: 0.5em;\n  height: 1px;\n  background-color: #589632;\n  animation: loading 1.5s ease-in-out infinite;\n  transform-origin: center;\n}\n\n.demo.spinner:fullscreen::before {\n  bottom: 0.25em;\n}\n\n.canvas-options {\n  margin: -1em 0 2em 0;\n  padding: 1em;\n  border: 1px solid #ccc;\n  background-color: #ffffff;\n  text-align: center;\n}\n\n.canvas-options .seperator {\n  display: inline-block;\n  margin: 0 0.5em;\n  border-left: 1px solid #ccc;\n}\n\n.code {\n  overflow: auto;\n  border: 1px solid #ccc;\n  padding: 0.5em 1em;\n  font-size: medium;\n}\n\n#layers-control {\n  position: fixed;\n  bottom: 0;\n  right: 2em;\n}\n\n#layers-control .layers,\n.themes {\n  width: 25em;\n  padding: 1em 0.5em;\n  border: 1px solid #ccc;\n  border-bottom: none;\n  background-color: #ffffff;\n}\n\n#layers-control .layers .layers-toolbar {\n  float: right;\n}\n\n#layers-control .layers .layers-download {\n  cursor: pointer;\n  user-select: none;\n  font-size: 0.75em;\n  opacity: 0.3;\n}\n\n#layers-control .layers .layers-download:hover {\n  opacity: 0.7;\n  color: #589632;\n}\n\n#layers-control .layers::before,\n.themes::before {\n  margin: 1em 0.5em;\n  font-weight: bold;\n  content: \"Layers:\";\n}\n\n#layers-control .themes::before {\n  content: \"Map Theme:\";\n}\n\n#layers-control .themes select {\n  float: right;\n  width: 18em;\n}\n\n#layers-control .layers .layer-tree {\n  margin-top: 0.5em;\n  font-size: 0.85em;\n}\n\n#layers-control .layers .layer-header {\n  display: flex;\n  align-items: center;\n  padding: 2px 0;\n  gap: 4px;\n  white-space: nowrap;\n}\n\n#layers-control .layers .layer-header.disabled {\n  opacity: 0.4;\n}\n\n#layers-control .layers .layer-toggle {\n  cursor: pointer;\n  user-select: none;\n  width: 1em;\n  text-align: center;\n  flex-shrink: 0;\n}\n\n#layers-control .layers .layer-name {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  flex: 1;\n  min-width: 0;\n}\n\n#layers-control .layers .layer-name.is-group {\n  font-weight: 600;\n}\n\n#layers-control .layers .layer-slider {\n  width: 5em;\n  flex-shrink: 0;\n}\n\n#layers-control .layers .layer-filter-icon {\n  cursor: pointer;\n  user-select: none;\n  flex-shrink: 0;\n  font-size: 0.9em;\n  opacity: 0.3;\n}\n\n#layers-control .layers .layer-filter-icon:hover {\n  opacity: 0.7;\n}\n\n#layers-control .layers .layer-filter-icon.active {\n  opacity: 1;\n  color: #589632;\n}\n\n#layers-control .layers .layer-filter {\n  padding-left: 2.2em;\n  padding-bottom: 2px;\n}\n\n#layers-control .layers .layer-filter input {\n  width: 100%;\n  font-size: 0.85em;\n  padding: 2px 4px;\n  border: 1px solid #ccc;\n  border-radius: 2px;\n}\n\n#layers-control .layers .layer-filter input.filter-error {\n  border-color: #c00;\n  background-color: #fee;\n}\n\n#layers-control .layers .layer-legend-icon {\n  cursor: pointer;\n  user-select: none;\n  flex-shrink: 0;\n  font-size: 0.75em;\n  opacity: 0.3;\n}\n\n#layers-control .layers .layer-legend-icon:hover {\n  opacity: 0.7;\n}\n\n#layers-control .layers .layer-legend-icon.active {\n  opacity: 1;\n  color: #589632;\n}\n\n#layers-control .layers .layer-legend {\n  padding-left: 2.2em;\n  margin: 2px 0;\n}\n\n#layers-control .layers .legend-item {\n  display: flex;\n  align-items: center;\n  gap: 4px;\n  padding: 1px 0;\n  font-size: 0.8em;\n  color: #666;\n}\n\n#layers-control .layers .legend-icon {\n  width: 16px;\n  height: 16px;\n  flex-shrink: 0;\n}\n\n#layers-control .layers .legend-label {\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n#layers-control .layers .layer-children {\n  padding-left: 1.2em;\n  border-left: 1px solid #ddd;\n  margin-left: 0.45em;\n}\n\n#js-demo-canvas {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  width: 100%;\n  height: 100%;\n}\n\n#js-demo-controls {\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  padding: 1em;\n  text-align: center;\n}\n\n.tabset > input[type=\"radio\"] {\n  position: absolute;\n  left: -200vw;\n}\n\n.tabset .tab-panel {\n  display: none;\n}\n\n.tabset > input:first-child:checked ~ .tab-panels > .tab-panel:first-child,\n.tabset > input:nth-child(3):checked ~ .tab-panels > .tab-panel:nth-child(2),\n.tabset > input:nth-child(5):checked ~ .tab-panels > .tab-panel:nth-child(3),\n.tabset > input:nth-child(7):checked ~ .tab-panels > .tab-panel:nth-child(4),\n.tabset > input:nth-child(9):checked ~ .tab-panels > .tab-panel:nth-child(5),\n.tabset > input:nth-child(11):checked ~ .tab-panels > .tab-panel:nth-child(6) {\n  display: block;\n}\n\n.tabset > label {\n  position: relative;\n  display: inline-block;\n  padding: 15px 15px 25px;\n  border: 1px solid transparent;\n  border-bottom: 0;\n  cursor: pointer;\n  font-weight: 600;\n}\n\n.tabset > label::after {\n  content: \"\";\n  position: absolute;\n  left: 15px;\n  bottom: 10px;\n  width: 22px;\n  height: 4px;\n  background: #8d8d8d;\n}\n\ninput:focus-visible + label {\n  outline: 2px solid #589632;\n  border-radius: 3px;\n}\n\n.tabset > label:hover,\n.tabset > input:focus + label,\n.tabset > input:checked + label {\n  color: #589632;\n}\n\n.tabset > label:hover::after,\n.tabset > input:focus + label::after,\n.tabset > input:checked + label::after {\n  background: #589632;\n}\n\n.tabset > input:checked + label {\n  border-color: #ccc;\n  border-bottom: 1px solid #fff;\n  margin-bottom: -1px;\n}\n\n.tab-panel {\n  padding: 30px 0;\n  border-top: 1px solid #ccc;\n}\n\n*,\n*:before,\n*:after {\n  box-sizing: border-box;\n}\n\nbody {\n  padding: 30px;\n}\n\n.tabset {\n  position: relative;\n  max-width: 65em;\n}\n\n#project {\n  position: absolute;\n  top: 0;\n  right: 0;\n  padding: 15px 0;\n}\n\n#project label {\n  margin-right: 1em;\n}\n\n#project > div {\n  display: inline;\n  padding-right: 15px;\n}\n\n#projects {\n  min-width: 15em;\n  max-width: 20em;\n}\n\n#status {\n  position: absolute;\n  top: 0;\n  right: 0;\n  padding: 15px 0;\n}\n\n#status div {\n  display: inline-block;\n}\n\n.lds-ellipsis {\n  display: inline-block;\n  position: relative;\n  width: 80px;\n  height: 1em;\n  margin-left: 0.5em;\n  padding-top: 0.25em;\n}\n.lds-ellipsis div {\n  position: absolute;\n  width: 13px;\n  height: 13px;\n  border-radius: 50%;\n  background: #589632;\n  animation-timing-function: cubic-bezier(0, 1, 1, 0);\n}\n.lds-ellipsis div:nth-child(1) {\n  left: 8px;\n  animation: lds-ellipsis1 0.6s infinite;\n}\n.lds-ellipsis div:nth-child(2) {\n  left: 8px;\n  animation: lds-ellipsis2 0.6s infinite;\n}\n.lds-ellipsis div:nth-child(3) {\n  left: 32px;\n  animation: lds-ellipsis2 0.6s infinite;\n}\n.lds-ellipsis div:nth-child(4) {\n  left: 56px;\n  animation: lds-ellipsis3 0.6s infinite;\n}\n@keyframes lds-ellipsis1 {\n  0% {\n    transform: scale(0);\n  }\n  100% {\n    transform: scale(1);\n  }\n}\n@keyframes lds-ellipsis3 {\n  0% {\n    transform: scale(1);\n  }\n  100% {\n    transform: scale(0);\n  }\n}\n@keyframes lds-ellipsis2 {\n  0% {\n    transform: translate(0, 0);\n  }\n  100% {\n    transform: translate(24px, 0);\n  }\n}\n\n/*!\n * \"Fork me on GitHub\" CSS ribbon v0.2.3 | MIT License\n * https://github.com/simonwhitaker/github-fork-ribbon-css\n*/\n\n.github-fork-ribbon {\n  width: 12.1em;\n  height: 12.1em;\n  position: absolute;\n  overflow: hidden;\n  top: 0;\n  right: 0;\n  z-index: 9999;\n  pointer-events: none;\n  font-size: 13px;\n  text-decoration: none;\n  text-indent: -999999px;\n}\n\n.github-fork-ribbon.fixed {\n  position: fixed;\n}\n\n.github-fork-ribbon:hover,\n.github-fork-ribbon:active {\n  background-color: rgba(0, 0, 0, 0);\n}\n\n.github-fork-ribbon:before,\n.github-fork-ribbon:after {\n  /* The right and left classes determine the side we attach our banner to */\n  position: absolute;\n  display: block;\n  width: 15.38em;\n  height: 1.54em;\n\n  top: 3.23em;\n  right: -3.23em;\n\n  -webkit-box-sizing: content-box;\n  -moz-box-sizing: content-box;\n  box-sizing: content-box;\n\n  -webkit-transform: rotate(45deg);\n  -moz-transform: rotate(45deg);\n  -ms-transform: rotate(45deg);\n  -o-transform: rotate(45deg);\n  transform: rotate(45deg);\n}\n\n.github-fork-ribbon:before {\n  content: \"\";\n\n  /* Add a bit of padding to give some substance outside the \"stitching\" */\n  padding: 0.38em 0;\n\n  /* Set the base colour */\n  background-color: #589632;\n\n  /* Set a gradient: transparent black at the top to almost-transparent black at the bottom */\n  background-image: -webkit-gradient(\n    linear,\n    left top,\n    left bottom,\n    from(rgba(0, 0, 0, 0)),\n    to(rgba(0, 0, 0, 0.15))\n  );\n  background-image: -webkit-linear-gradient(\n    top,\n    rgba(0, 0, 0, 0),\n    rgba(0, 0, 0, 0.15)\n  );\n  background-image: -moz-linear-gradient(\n    top,\n    rgba(0, 0, 0, 0),\n    rgba(0, 0, 0, 0.15)\n  );\n  background-image: -ms-linear-gradient(\n    top,\n    rgba(0, 0, 0, 0),\n    rgba(0, 0, 0, 0.15)\n  );\n  background-image: -o-linear-gradient(\n    top,\n    rgba(0, 0, 0, 0),\n    rgba(0, 0, 0, 0.15)\n  );\n  background-image: linear-gradient(\n    to bottom,\n    rgba(0, 0, 0, 0),\n    rgba(0, 0, 0, 0.15)\n  );\n\n  /* Add a drop shadow */\n  -webkit-box-shadow: 0 0.15em 0.23em 0 rgba(0, 0, 0, 0.5);\n  -moz-box-shadow: 0 0.15em 0.23em 0 rgba(0, 0, 0, 0.5);\n  box-shadow: 0 0.15em 0.23em 0 rgba(0, 0, 0, 0.5);\n\n  pointer-events: auto;\n}\n\n.github-fork-ribbon:after {\n  /* Set the text from the data-ribbon attribute */\n  content: attr(data-ribbon);\n\n  /* Set the text properties */\n  color: #fff;\n  font:\n    700 1em \"Helvetica Neue\",\n    Helvetica,\n    Arial,\n    sans-serif;\n  line-height: 1.54em;\n  text-decoration: none;\n  text-shadow: 0 -0.08em rgba(0, 0, 0, 0.5);\n  text-align: center;\n  text-indent: 0;\n\n  /* Set the layout properties */\n  padding: 0.15em 0;\n  margin: 0.15em 0;\n\n  /* Add \"stitching\" effect */\n  border-width: 0.08em 0;\n  border-style: dotted;\n  border-color: #fff;\n  border-color: rgba(255, 255, 255, 0.7);\n}\n\n.github-fork-ribbon.left-top,\n.github-fork-ribbon.left-bottom {\n  right: auto;\n  left: 0;\n}\n\n.github-fork-ribbon.left-bottom,\n.github-fork-ribbon.right-bottom {\n  top: auto;\n  bottom: 0;\n}\n\n.github-fork-ribbon.left-top:before,\n.github-fork-ribbon.left-top:after,\n.github-fork-ribbon.left-bottom:before,\n.github-fork-ribbon.left-bottom:after {\n  right: auto;\n  left: -3.23em;\n}\n\n.github-fork-ribbon.left-bottom:before,\n.github-fork-ribbon.left-bottom:after,\n.github-fork-ribbon.right-bottom:before,\n.github-fork-ribbon.right-bottom:after {\n  top: auto;\n  bottom: 3.23em;\n}\n\n.github-fork-ribbon.left-top:before,\n.github-fork-ribbon.left-top:after,\n.github-fork-ribbon.right-bottom:before,\n.github-fork-ribbon.right-bottom:after {\n  -webkit-transform: rotate(-45deg);\n  -moz-transform: rotate(-45deg);\n  -ms-transform: rotate(-45deg);\n  -o-transform: rotate(-45deg);\n  transform: rotate(-45deg);\n}\n\n.github-fork-ribbon:before {\n  background-color: #589632;\n}\n"
  },
  {
    "path": "sites/dev/src/index.ts",
    "content": "import { QGIS_JS_VERSION, qgis } from \"qgis-js\";\n\nimport { QgisApi } from \"qgis-js\";\n\nimport { useProjects } from \"@qgis-js/utils\";\nimport type { Project } from \"@qgis-js/utils\";\n\nimport { jsDemo } from \"./js\";\n\nimport { olPreview, olDemoXYZ, olDemoCanvas } from \"./ol\";\nimport { layersControl } from \"./layers\";\n\nconst printVersion = true;\nconst apiTest = false;\nconst timer = false;\n\nfunction isDev() {\n  // @ts-ignore\n  return import.meta.env.MODE === \"development\";\n}\n\nconst qgisJsDemoProjects = (path: string) => ({\n  owner: \"boardend\",\n  repo: \"qgis-js-projects\",\n  path: \"/\" + path,\n  branch: \"main\",\n  prefix: `${path[0].toUpperCase()}${path.slice(1)}: `,\n});\n\nconst GITHUB_REPOS: Array<{\n  owner: string;\n  repo: string;\n  path?: string;\n  branch?: string;\n  prefix?: string;\n}> = [\n  qgisJsDemoProjects(\"demo\"),\n  ...(isDev()\n    ? [\"test\", \"performance\"].map((path) => qgisJsDemoProjects(path))\n    : []),\n];\n\nfunction testApi(api: QgisApi) {\n  const p1 = new api.QgsPointXY();\n  console.dir(p1);\n\n  const r1 = new api.QgsRectangle();\n  console.log(r1);\n\n  const r2 = new api.QgsRectangle(1, 2, 3, 4);\n  console.log(r2.xMinimum, r2.yMinimum, r2.xMaximum, r2.yMaximum);\n  r2.scale(5);\n  console.log(r2.xMinimum, r2.yMinimum, r2.xMaximum, r2.yMaximum);\n}\n\nasync function initDemo() {\n  if (printVersion) {\n    console.log(`qgis-js (${QGIS_JS_VERSION})`);\n  }\n\n  const statusControl = document.getElementById(\"status\")! as HTMLDivElement;\n  const projectControl = document.getElementById(\"project\")! as HTMLDivElement;\n\n  let isError = false;\n  const onStatus = (status: string) => {\n    if (isError) return;\n    (statusControl.firstElementChild! as HTMLDivElement).innerHTML = status;\n  };\n  const onError = (error: Error | any) => {\n    isError = true;\n    console.error(error);\n    const message =\n      \"\" + error && error[\"message\"] ? error[\"message\"] : \"Runtime error\";\n    projectControl.style.visibility = \"none\";\n    statusControl.style.display = \"auto\";\n    statusControl.innerHTML = `<div class=\"alert alert-danger\" role=\"alert\">\n      <b style=\"color: red\">Error:&nbsp;</b>\n      ${message}\n    </div>`;\n  };\n  const onReady = () => {\n    statusControl.style.display = \"none\";\n    projectControl.style.visibility = \"visible\";\n  };\n\n  try {\n    // boot the runtime\n    if (timer) console.time(\"boot\");\n    const { api, fs } = await qgis({\n      // use assets form QgisRuntimePlugin\n      prefix: new URL(\"assets/wasm\", window.location.href).pathname,\n      onStatus: (status: string) => onStatus(status),\n    });\n    if (timer) console.timeEnd(\"boot\");\n\n    // prepare project management\n    onStatus(\"Loading projects...\");\n    const updateCallbacks: Array<Function> = [];\n    const renderCallbacks: Array<Function> = [];\n\n    const {\n      openProject,\n      loadLocalProject,\n      loadRemoteProjects,\n      loadGithubProjects,\n    } = useProjects(fs, (project: string) => {\n      if (timer) console.time(\"project\");\n      api.loadProject(project);\n      if (timer) console.timeEnd(\"project\");\n\n      // update all demos\n      setTimeout(() => {\n        updateCallbacks.forEach((update) => update());\n      }, 0);\n    });\n\n    const projects = new Map<string, () => Project | Promise<Project>>();\n    const projectSelect = document.getElementById(\n      \"projects\",\n    )! as HTMLSelectElement;\n    projectSelect.addEventListener(\"change\", () => {\n      const project = projects.get(projectSelect.value);\n      if (project) {\n        openProject(project());\n      }\n    });\n    const listProject = (\n      name: string,\n      projectLoadFunciton: () => Project | Promise<Project>,\n    ) => {\n      projects.set(name, projectLoadFunciton);\n      const option = document.createElement(\"option\");\n      option.value = name;\n      option.text = name;\n      projectSelect.add(option, null);\n    };\n    document.getElementById(\"local-project\")!.onclick = async function () {\n      const localProject = await loadLocalProject();\n      await openProject(localProject);\n      listProject(localProject.name, () => localProject);\n      projectSelect.value = localProject.name;\n    };\n\n    // load remote projects\n    if (timer) console.time(\"remote projects\");\n    // - remote projects\n    const remoteProjects = await loadRemoteProjects();\n    remoteProjects.forEach((project) =>\n      listProject(project.name, () => project),\n    );\n    if (timer) console.timeEnd(\"remote projects\");\n\n    // - github projects\n    if (timer) console.time(\"github projects\");\n    for (const repo of GITHUB_REPOS) {\n      try {\n        const githubProjects = await loadGithubProjects(\n          repo.owner,\n          repo.repo,\n          repo.path,\n          repo.branch,\n        );\n        Object.entries(githubProjects).forEach(([name, projectLoadPromise]) => {\n          listProject((repo.prefix || \"\") + name, projectLoadPromise);\n        });\n      } catch (error) {\n        console.warn(\n          `Unable to load GitHub project \"${repo.owner}/${repo.repo}\"`,\n          error,\n        );\n      }\n    }\n    if (timer) console.timeEnd(\"github projects\");\n\n    // open first project\n    onStatus(\"Opening first project...\");\n    await openProject(remoteProjects[0]);\n\n    // API tests\n    if (apiTest) testApi(api);\n\n    // paint a first dummy frame\n    onStatus(\"Rendering first frame...\");\n    if (timer) console.time(\"first frame\");\n    await api.renderImage(api.srid(), api.fullExtent(), 42, 42, 1);\n    if (timer) console.timeEnd(\"first frame\");\n\n    onReady();\n\n    const layersControlDiv = document.getElementById(\n      \"layers-control\",\n    ) as HTMLDivElement | null;\n    if (layersControlDiv) {\n      updateCallbacks.push(\n        layersControl(layersControlDiv, api, () => {\n          // update all demos\n          setTimeout(() => {\n            renderCallbacks.forEach((render) => render());\n          }, 0);\n        }),\n      );\n    }\n\n    // js demo\n    const jsDemoCanvas = document.getElementById(\n      \"js-demo-canvas\",\n    ) as HTMLCanvasElement | null;\n    if (jsDemoCanvas) {\n      const { update, render } = jsDemo(jsDemoCanvas, api);\n      updateCallbacks.push(update);\n      renderCallbacks.push(render);\n      // ensure js demo gets refreshed when the section gets visible\n      const jsButton = document.getElementById(\"tab1\") as HTMLInputElement;\n      jsButton.addEventListener(\"change\", () => {\n        if (jsButton.checked) update();\n      });\n    }\n\n    // ol demo\n    const olDemoPreviewDiv = document.getElementById(\n      \"ol-demo-preview\",\n    ) as HTMLDivElement | null;\n    if (olDemoPreviewDiv) {\n      const { update, render } = olPreview(olDemoPreviewDiv, api);\n      updateCallbacks.push(update);\n      renderCallbacks.push(render);\n    }\n\n    const olDemoCanvasDiv = document.getElementById(\n      \"ol-demo-canvas\",\n    ) as HTMLDivElement | null;\n    if (olDemoCanvasDiv) {\n      const { update, render } = olDemoCanvas(olDemoCanvasDiv, api);\n      updateCallbacks.push(update);\n      renderCallbacks.push(render);\n    }\n\n    const olDemoXYZDiv = document.getElementById(\n      \"ol-demo-xyz\",\n    ) as HTMLDivElement | null;\n    if (olDemoXYZDiv) {\n      const { update, render } = olDemoXYZ(olDemoXYZDiv, api);\n      updateCallbacks.push(update);\n      renderCallbacks.push(render);\n    }\n  } catch (error) {\n    onError(error);\n  }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", function () {\n  initDemo();\n});\n"
  },
  {
    "path": "sites/dev/src/js.ts",
    "content": "import { QgisApi } from \"qgis-js\";\n\nimport type { QgsRectangle } from \"qgis-js\";\n\nconst mapScaleFactor = 1.5;\nconst mapMoveFactor = 0.1;\n\nexport function jsDemo(\n  canvas: HTMLCanvasElement,\n  api: QgisApi,\n): { update: () => void; render: () => void } {\n  let lastExtent: QgsRectangle | null = null;\n\n  // ensure pixel perfect rendering\n  // see https://web.dev/articles/device-pixel-content-box\n  const observer = new ResizeObserver((entries) => {\n    const entry = entries.find((entry) => entry.target === canvas);\n    if (entry) {\n      canvas.width = entry.devicePixelContentBoxSize[0].inlineSize;\n      canvas.height = entry.devicePixelContentBoxSize[0].blockSize;\n    }\n    renderMap();\n  });\n  observer.observe(canvas, { box: \"device-pixel-content-box\" });\n\n  async function renderMap() {\n    var devicePixelRatio = window.devicePixelRatio || 1;\n    var cssRect = canvas.getBoundingClientRect();\n    const imageWidth = cssRect.width * devicePixelRatio;\n    const imageHeight = cssRect.height * devicePixelRatio;\n\n    if (imageWidth && imageHeight) {\n      const image = await api.renderImage(\n        api.srid(),\n        lastExtent!,\n        imageWidth,\n        imageHeight,\n        window.devicePixelRatio,\n      );\n\n      const context = canvas.getContext(\"2d\");\n      canvas.width = imageWidth;\n      canvas.height = imageHeight;\n      context!.scale(devicePixelRatio, devicePixelRatio);\n      context!.putImageData(image, 0, 0);\n    }\n  }\n\n  document.getElementById(\"zoomin\")!.onclick = function () {\n    lastExtent!.scale(1 / mapScaleFactor);\n    renderMap();\n  };\n  document.getElementById(\"zoomout\")!.onclick = function () {\n    lastExtent!.scale(mapScaleFactor);\n    renderMap();\n  };\n  document.getElementById(\"panleft\")!.onclick = function () {\n    lastExtent!.move(\n      (lastExtent!.xMaximum - lastExtent!.xMinimum) * mapMoveFactor,\n      0,\n    );\n    renderMap();\n  };\n  document.getElementById(\"panright\")!.onclick = function () {\n    lastExtent!.move(\n      -(lastExtent!.xMaximum - lastExtent!.xMinimum) * mapMoveFactor,\n      0,\n    );\n    renderMap();\n  };\n  document.getElementById(\"panup\")!.onclick = function () {\n    lastExtent!.move(\n      0,\n      -(lastExtent!.yMaximum - lastExtent!.yMinimum) * mapMoveFactor,\n    );\n    renderMap();\n  };\n  document.getElementById(\"pandown\")!.onclick = function () {\n    lastExtent!.move(\n      0,\n      (lastExtent!.yMaximum - lastExtent!.yMinimum) * mapMoveFactor,\n    );\n    renderMap();\n  };\n\n  function onStart() {\n    lastExtent = api.fullExtent();\n    renderMap();\n  }\n\n  onStart();\n\n  return {\n    update: () => {\n      onStart();\n    },\n    render: () => {\n      renderMap();\n    },\n  };\n}\n"
  },
  {
    "path": "sites/dev/src/layers.ts",
    "content": "import type {\n  QgisApi,\n  QgsLayerTreeNode,\n  QgsLayerTreeLayer,\n  QgsVectorLayer,\n} from \"qgis-js\";\nimport { LayerType } from \"qgis-js\";\n\nfunction renderNode(\n  parent: HTMLElement,\n  node: QgsLayerTreeNode,\n  redraw: () => void,\n  update: () => void,\n  parentVisible: boolean = true,\n) {\n  const el = document.createElement(\"div\");\n\n  const header = document.createElement(\"div\");\n  header.className = \"layer-header\";\n  const visible = parentVisible && node.itemVisibilityChecked;\n  if (!visible) header.classList.add(\"disabled\");\n\n  if (node.isGroup()) {\n    const toggle = document.createElement(\"span\");\n    toggle.className = \"layer-toggle\";\n    toggle.textContent = node.expanded ? \"\\u25BE\" : \"\\u25B8\";\n    toggle.addEventListener(\"click\", () => {\n      node.expanded = !node.expanded;\n      update();\n    });\n    header.appendChild(toggle);\n  } else {\n    const spacer = document.createElement(\"span\");\n    spacer.className = \"layer-toggle\";\n    header.appendChild(spacer);\n  }\n\n  const checkbox = document.createElement(\"input\");\n  checkbox.type = \"checkbox\";\n  checkbox.checked = node.itemVisibilityChecked;\n  checkbox.addEventListener(\"change\", () => {\n    node.itemVisibilityChecked = checkbox.checked;\n    redraw();\n    update();\n  });\n  header.appendChild(checkbox);\n\n  const name = document.createElement(\"span\");\n  name.className = \"layer-name\";\n  if (node.isGroup()) name.classList.add(\"is-group\");\n  name.textContent = node.name;\n  header.appendChild(name);\n\n  let filterRow: HTMLDivElement | undefined;\n  let legendContainer: HTMLDivElement | undefined;\n  if (node.isLayer()) {\n    const treeLayer = node as QgsLayerTreeLayer;\n    const mapLayer = treeLayer.layer();\n    if (mapLayer && mapLayer.type() === LayerType.Vector) {\n      const vectorLayer = mapLayer as QgsVectorLayer;\n      const hasFilter = vectorLayer.subsetString() !== \"\";\n\n      const filterIcon = document.createElement(\"span\");\n      filterIcon.className = \"layer-filter-icon\";\n      if (hasFilter) filterIcon.classList.add(\"active\");\n      filterIcon.title = \"SQL filter\";\n      filterIcon.textContent = \"\\u2AF6\";\n      header.appendChild(filterIcon);\n\n      filterRow = document.createElement(\"div\");\n      filterRow.className = \"layer-filter\";\n      filterRow.style.display = \"none\";\n\n      const filterInput = document.createElement(\"input\");\n      filterInput.type = \"text\";\n      filterInput.placeholder = \"SQL filter expression\";\n      filterInput.value = vectorLayer.subsetString();\n      const applyFilter = () => {\n        const success = vectorLayer.setSubsetString(filterInput.value);\n        filterInput.classList.toggle(\"filter-error\", !success);\n        filterIcon.classList.toggle(\"active\", filterInput.value !== \"\");\n        if (success) redraw();\n      };\n      filterInput.addEventListener(\"change\", applyFilter);\n      filterRow.appendChild(filterInput);\n\n      filterIcon.addEventListener(\"click\", () => {\n        const isVisible = filterRow!.style.display !== \"none\";\n        filterRow!.style.display = isVisible ? \"none\" : \"\";\n        if (!isVisible) filterInput.focus();\n      });\n    }\n\n    const legendIcon = document.createElement(\"span\");\n    legendIcon.className = \"layer-legend-icon\";\n    legendIcon.title = \"Legend\";\n    legendIcon.textContent = \"\\u2630\";\n    header.appendChild(legendIcon);\n\n    legendContainer = document.createElement(\"div\");\n    legendContainer.className = \"layer-legend\";\n    legendContainer.style.display = \"none\";\n\n    legendIcon.addEventListener(\"click\", () => {\n      const isVisible = legendContainer!.style.display !== \"none\";\n      legendContainer!.style.display = isVisible ? \"none\" : \"\";\n      legendIcon.classList.toggle(\"active\", !isVisible);\n      if (!isVisible && legendContainer!.childElementCount === 0) {\n        for (const legendNode of treeLayer.legendNodes()) {\n          const row = document.createElement(\"div\");\n          row.className = \"legend-item\";\n          const icon = legendNode.symbolImage(16);\n          if (icon) {\n            const img = document.createElement(\"img\");\n            img.className = \"legend-icon\";\n            img.src = icon;\n            row.appendChild(img);\n          }\n          const label = document.createElement(\"span\");\n          label.className = \"legend-label\";\n          label.textContent = legendNode.label();\n          row.appendChild(label);\n          legendContainer!.appendChild(row);\n        }\n      }\n    });\n\n    if (mapLayer) {\n      const slider = document.createElement(\"input\");\n      slider.className = \"layer-slider\";\n      slider.type = \"range\";\n      slider.min = \"0\";\n      slider.max = \"100\";\n      slider.value = \"\" + mapLayer.opacity * 100;\n      slider.step = \"1\";\n      slider.addEventListener(\"change\", () => {\n        mapLayer.opacity = parseInt(slider.value) / 100;\n        redraw();\n      });\n      header.appendChild(slider);\n    }\n  }\n\n  el.appendChild(header);\n  if (filterRow) el.appendChild(filterRow);\n  if (legendContainer) el.appendChild(legendContainer);\n\n  if (node.isGroup()) {\n    const childContainer = document.createElement(\"div\");\n    childContainer.className = \"layer-children\";\n    if (!node.expanded) childContainer.style.display = \"none\";\n    const children = node.children();\n    for (const child of children) {\n      renderNode(childContainer, child, redraw, update, visible);\n    }\n    el.appendChild(childContainer);\n  }\n\n  parent.appendChild(el);\n}\n\nexport function layersControl(\n  target: HTMLDivElement,\n  api: QgisApi,\n  redraw: () => void = () => {},\n): () => void {\n  const update = () => {\n    target.innerHTML = \"\";\n\n    const layerContainer = document.createElement(\"div\");\n    layerContainer.className = \"layers\";\n    target.appendChild(layerContainer);\n\n    const toolbar = document.createElement(\"div\");\n    toolbar.className = \"layers-toolbar\";\n\n    const downloadIcon = document.createElement(\"span\");\n    downloadIcon.className = \"layers-download\";\n    downloadIcon.title = \"Download legend as PNG\";\n    downloadIcon.textContent = \"\\u2630\";\n    downloadIcon.addEventListener(\"click\", () => {\n      const dataUrl = api.renderLegend(300);\n      if (!dataUrl) return;\n      const link = document.createElement(\"a\");\n      link.href = dataUrl;\n      link.download = \"legend.png\";\n      link.style.display = \"none\";\n      document.body.appendChild(link);\n      link.click();\n      setTimeout(() => document.body.removeChild(link), 100);\n    });\n    toolbar.appendChild(downloadIcon);\n    layerContainer.appendChild(toolbar);\n\n    const tree = document.createElement(\"div\");\n    tree.className = \"layer-tree\";\n    layerContainer.appendChild(tree);\n\n    const root = api.layerTreeRoot();\n    for (const child of root.children()) {\n      renderNode(tree, child, redraw, update);\n    }\n\n    if (api.mapThemes().length > 0) {\n      const themeContainer = document.createElement(\"div\");\n      themeContainer.className = \"themes\";\n      target.appendChild(themeContainer);\n\n      const select = document.createElement(\"select\");\n      select.addEventListener(\"change\", () => {\n        if (select.value) {\n          api.setMapTheme(select.value);\n          redraw();\n          update();\n        }\n      });\n      themeContainer.appendChild(select);\n\n      const currentTheme = api.getMapTheme();\n\n      const option = document.createElement(\"option\");\n      option.value = \"\";\n      option.text = \"\";\n      if (!currentTheme) {\n        option.selected = true;\n      }\n      select.appendChild(option);\n\n      for (const theme of api.mapThemes()) {\n        const option = document.createElement(\"option\");\n        option.value = theme;\n        option.text = theme;\n        if (theme === currentTheme) {\n          option.selected = true;\n        }\n        select.appendChild(option);\n      }\n    }\n  };\n\n  update();\n\n  return update;\n}\n"
  },
  {
    "path": "sites/dev/src/ol.ts",
    "content": "import { QgisApi } from \"qgis-js\";\n\nimport {\n  QgisJobDataSource,\n  QgisCanvasDataSource,\n  QgisXYZDataSource,\n} from \"@qgis-js/ol\";\n\nimport Map from \"ol/Map.js\";\nimport View from \"ol/View.js\";\n\nimport WebGLTileLayer from \"ol/layer/WebGLTile.js\";\nimport ImageLayer from \"ol/layer/Image\";\n\nimport XYZ from \"ol/source/XYZ.js\";\n\nimport Projection from \"ol/proj/Projection.js\";\n\nimport {\n  ScaleLine,\n  FullScreen,\n  defaults as defaultControls,\n} from \"ol/control.js\";\n\n// @ts-ignore\nimport(\"ol/ol.css\");\n\nconst animationDuration = 500;\n\nconst useBaseMap = true;\n\nexport function olDemoXYZ(\n  target: HTMLDivElement,\n  api: QgisApi,\n): { init: () => void; update: () => void; render: () => void } {\n  let view: View | undefined = undefined;\n  let map: Map | undefined = undefined;\n  let layer: WebGLTileLayer | undefined = undefined;\n  let source: QgisXYZDataSource | undefined = undefined;\n\n  const getBbox = () => {\n    const initioalSrid = api.srid();\n    const initialExtent = api.fullExtent();\n    return initioalSrid === \"EPSG:3857\"\n      ? initialExtent\n      : api.transformRectangle(initialExtent, initioalSrid, \"EPSG:3857\");\n  };\n\n  const init = () => {\n    target.innerHTML = \"\";\n\n    const center = getBbox().center();\n\n    view = new View({\n      center: [center.x, center.y],\n      zoom: 10,\n    });\n\n    source = new QgisXYZDataSource(api, {\n      debug: false,\n      extentBufferFactor: () => {\n        const input = document.getElementById(\n          \"extentBufferFactor\",\n        ) as HTMLInputElement;\n        return input.valueAsNumber;\n      },\n    });\n\n    ((layer = new WebGLTileLayer({\n      source,\n    })),\n      (map = new Map({\n        target,\n        view,\n        controls: defaultControls().extend([new ScaleLine(), new FullScreen()]),\n        layers: [\n          layer,\n          new WebGLTileLayer({\n            visible: useBaseMap,\n            source: new XYZ({\n              url: `https://tile.openstreetmap.org/{z}/{x}/{y}.png`,\n            }),\n            style: {\n              saturation: [\"var\", \"saturation\"],\n              variables: {\n                saturation: -0.75,\n              },\n            },\n          }),\n        ].reverse(),\n      })));\n\n    map.on(\"loadstart\", function () {\n      map!.getTargetElement().classList.add(\"spinner\");\n    });\n    map.on(\"loadend\", function () {\n      map!.getTargetElement().classList.remove(\"spinner\");\n    });\n\n    map.once(\"precompose\", function (_event) {\n      // fit the view to the extent of the data once the map gets actually rendered\n      update();\n    });\n  };\n\n  const update = () => {\n    const bbox = getBbox();\n    view!.fit([bbox.xMinimum, bbox.yMinimum, bbox.xMaximum, bbox.yMaximum], {\n      duration: animationDuration,\n    });\n    setTimeout(() => {\n      render();\n    }, 0);\n  };\n\n  const render = () => {\n    source?.clear();\n    layer?.getRenderer()?.clearCache();\n    layer?.changed();\n  };\n\n  const xyzBaseMapCheckbox = document.getElementById(\n    \"xyzBaseMap\",\n  ) as HTMLInputElement | null;\n  if (xyzBaseMapCheckbox) {\n    xyzBaseMapCheckbox.addEventListener(\"change\", () => {\n      if (map)\n        map.getLayers().getArray()[0].setVisible(xyzBaseMapCheckbox.checked);\n    });\n  }\n\n  init();\n\n  return {\n    init,\n    update,\n    render,\n  };\n}\n\nexport function olDemoCanvas(\n  target: HTMLDivElement,\n  api: QgisApi,\n): { init: () => void; update: () => void; render: () => void } {\n  let view: View | undefined = undefined;\n  let srid: string | undefined = undefined;\n  let map: Map | undefined = undefined;\n  let layer: ImageLayer<QgisCanvasDataSource> | undefined = undefined;\n  let source: QgisCanvasDataSource | undefined = undefined;\n\n  const init = () => {\n    target.innerHTML = \"\";\n\n    srid = api.srid();\n\n    const projection = new Projection({\n      code: srid,\n      // TODO map unit of QgsCoordinateReferenceSystem to ol unit\n      // https://api.qgis.org/api/classQgsCoordinateReferenceSystem.html#ad57c8a9222c27173c7234ca270306128\n      // https://openlayers.org/en/latest/apidoc/module-ol_proj_Units.html\n      units: \"m\",\n    });\n\n    const bbox = api.fullExtent();\n    const center = bbox.center();\n\n    view = new View({\n      projection,\n      center: [center.x, center.y],\n      zoom: 10,\n    });\n\n    source = new QgisCanvasDataSource(api, {\n      projection,\n    });\n\n    layer = new ImageLayer({\n      source,\n    });\n\n    map = new Map({\n      target,\n      view,\n      controls: defaultControls().extend([new ScaleLine(), new FullScreen()]),\n      layers: [layer],\n    });\n\n    map.on(\"loadstart\", function () {\n      map!.getTargetElement().classList.add(\"spinner\");\n    });\n    map.on(\"loadend\", function () {\n      map!.getTargetElement().classList.remove(\"spinner\");\n    });\n\n    map.once(\"precompose\", function (_event) {\n      const bbox = api.fullExtent();\n      view!.fit([bbox.xMinimum, bbox.yMinimum, bbox.xMaximum, bbox.yMaximum], {\n        duration: animationDuration,\n      });\n    });\n  };\n\n  // recreate the entire map on each update to get new projections working\n  const update = () => {\n    init();\n  };\n\n  const render = () => {\n    setTimeout(() => {\n      // recreate the source to force reload the image in the layer\n      source = new QgisCanvasDataSource(api, {\n        projection: new Projection({\n          code: srid!,\n          units: \"m\",\n        }),\n      });\n      layer?.setSource(source);\n    }, 0);\n  };\n\n  init();\n\n  return {\n    init,\n    update,\n    render,\n  };\n}\n\nexport function olPreview(\n  target: HTMLDivElement,\n  api: QgisApi,\n): { init: () => void; update: () => void; render: () => void } {\n  let view: View | undefined = undefined;\n  let srid: string | undefined = undefined;\n  let map: Map | undefined = undefined;\n  let layer: ImageLayer<QgisJobDataSource> | undefined = undefined;\n  let source: QgisJobDataSource | undefined = undefined;\n\n  const inputPreview = document.getElementById(\n    \"previewRendering\",\n  ) as HTMLInputElement;\n  const inputTimeout = document.getElementById(\n    \"previewTimeout\",\n  ) as HTMLInputElement;\n  const inputOverlay = document.getElementById(\n    \"previewOverlay\",\n  ) as HTMLInputElement;\n\n  const init = () => {\n    target.innerHTML = \"\";\n\n    srid = api.srid();\n\n    const projection = new Projection({\n      code: srid,\n      // TODO map unit of QgsCoordinateReferenceSystem to ol unit\n      // https://api.qgis.org/api/classQgsCoordinateReferenceSystem.html#ad57c8a9222c27173c7234ca270306128\n      // https://openlayers.org/en/latest/apidoc/module-ol_proj_Units.html\n      units: \"m\",\n    });\n\n    const bbox = api.fullExtent();\n    const center = bbox.center();\n\n    view = new View({\n      projection,\n      center: [center.x, center.y],\n      zoom: 10,\n    });\n\n    source = new QgisJobDataSource(api, {\n      preview: inputPreview?.checked,\n      previewTimeout: inputTimeout?.valueAsNumber,\n      previewOverlay: inputOverlay?.checked,\n      projection,\n    });\n\n    layer = new ImageLayer({\n      source,\n    });\n\n    map = new Map({\n      target,\n      view,\n      controls: defaultControls().extend([new ScaleLine(), new FullScreen()]),\n      layers: [layer],\n    });\n\n    // @ts-ignore\n    source.on(\"jobstart\", function () {\n      map!.getTargetElement().classList.add(\"spinner\");\n    });\n    // @ts-ignore\n    source.on(\"jobend\", function () {\n      map!.getTargetElement().classList.remove(\"spinner\");\n    });\n\n    map.once(\"precompose\", function (_event) {\n      const bbox = api.fullExtent();\n      view!.fit([bbox.xMinimum, bbox.yMinimum, bbox.xMaximum, bbox.yMaximum], {\n        duration: animationDuration,\n      });\n    });\n  };\n\n  // recreate the entire map on each update to get new projections working\n  const update = () => {\n    if (source) {\n      source.killPendingJobs();\n    }\n    init();\n  };\n\n  inputPreview?.addEventListener(\"change\", () => update());\n  inputTimeout?.addEventListener(\"change\", () => update());\n  inputOverlay?.addEventListener(\"change\", () => update());\n\n  const render = () => {\n    setTimeout(() => {\n      // recreate the source to force reload the image in the layer\n      source = new QgisJobDataSource(api, {\n        preview: inputPreview?.checked,\n        previewTimeout: inputTimeout?.valueAsNumber,\n        previewOverlay: inputOverlay?.checked,\n        projection: new Projection({\n          code: srid!,\n          units: \"m\",\n        }),\n      });\n      layer?.setSource(source);\n\n      // @ts-ignore\n      source.on(\"jobstart\", function () {\n        map!.getTargetElement().classList.add(\"spinner\");\n      });\n      // @ts-ignore\n      source.on(\"jobend\", function () {\n        map!.getTargetElement().classList.remove(\"spinner\");\n      });\n    }, 0);\n  };\n\n  init();\n\n  return {\n    init,\n    update,\n    render,\n  };\n}\n"
  },
  {
    "path": "sites/dev/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "sites/dev/vite.config.ts",
    "content": "import { resolve } from \"path\";\nimport { defineConfig } from \"vite\";\n\nimport QgisRuntimePlugin from \"../../build/vite/QgisRuntimePlugin\";\nimport DirectoryListingPlugin from \"../../build/vite/DirectoryListingPlugin\";\nimport CrossOriginIsolationPlugin, {\n  CrossOriginIsolationResponseHeaders,\n} from \"../../build/vite/CrossOriginIsolationPlugin\";\n\nimport { viteStaticCopy } from \"vite-plugin-static-copy\";\n\nimport packageJson from \"./package.json\";\n\nexport default defineConfig({\n  base: \"/qgis-js/\",\n  define: {\n    __QGIS_JS_VERSION: JSON.stringify(packageJson.version),\n  },\n  resolve: {\n    alias: [\n      // don't use the bundlet version of qgis-js and qgis-js-ol to enable HMR\n      {\n        find: /^qgis-js$/,\n        replacement: resolve(__dirname, \"../../packages/qgis-js/src/index.ts\"),\n      },\n      {\n        find: /^@qgis-js\\/ol$/,\n        replacement: resolve(\n          __dirname,\n          \"../../packages/qgis-js-ol/src/index.ts\",\n        ),\n      },\n      {\n        find: /^@qgis-js\\/utils$/,\n        replacement: resolve(\n          __dirname,\n          \"../../packages/qgis-js-utils/src/index.ts\",\n        ),\n      },\n    ],\n  },\n  preview: {\n    headers: {\n      ...CrossOriginIsolationResponseHeaders,\n    },\n  },\n  plugins: [\n    QgisRuntimePlugin({\n      name: \"qgis-js\",\n      outputDir: \"build/wasm\",\n    }),\n    CrossOriginIsolationPlugin(),\n    DirectoryListingPlugin([\"public/projects\"]),\n    viteStaticCopy({\n      targets: [\n        {\n          src: \"node_modules/coi-serviceworker/coi-serviceworker.min.js\",\n          dest: \"\",\n        },\n      ],\n    }),\n  ],\n});\n"
  },
  {
    "path": "sites/performance/.gitignore",
    "content": "/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\n/test-results/\n/playwright-report/\n/blob-report/\n/playwright/.cache/\npublic/projects\n"
  },
  {
    "path": "sites/performance/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <title>qgis-js-performance</title>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"data:;base64,=\" />\n  </head>\n  <body>\n    <h1>qgis-js Performance Measurement Tool</h1>\n    <table>\n      <tr>\n        <td class=\"row-measure\">\n          <input id=\"boot-measure\" type=\"number\" disabled />\n        </td>\n        <td><button id=\"boot\">⚙️ Boot runtime</button></td>\n        <td class=\"row-label\">\n          <label for=\"boot-input\">Runtime</label>\n        </td>\n        <td class=\"row-input\">\n          <select id=\"boot-input\"></select>\n        </td>\n        <td></td>\n      </tr>\n      <tr>\n        <td><input id=\"project-measure\" type=\"number\" disabled /></td>\n        <td><button id=\"project\" disabled>Load project</button></td>\n        <td>\n          <label for=\"project-input\">Project</label>\n        </td>\n        <td>\n          <select id=\"project-input\">\n            <option disabled selected value></option>\n          </select>\n        </td>\n        <td><button>Add local project</button></td>\n      </tr>\n      <tr>\n        <td><input id=\"frame-measure\" type=\"number\" disabled /></td>\n        <td><button id=\"frame\" disabled>🖼️ Render first frame</button></td>\n        <td>\n          <label for=\"frame-input\">Resolution</label>\n        </td>\n        <td>\n          <input\n            id=\"frame-input\"\n            value=\"[42,42]\"\n            title=\"resplution\"\n            placeholder=\"resplution\"\n          />\n        </td>\n        <td></td>\n      </tr>\n      <tr>\n        <td rowspan=\"4\"></td>\n        <td><button id=\"start\">🏁 Start performance test</button></td>\n        <td><label for=\"start-extent-input\">Extents</label></td>\n        <td>\n          <input\n            id=\"start-extent-input\"\n            title=\"extent\"\n            placeholder=\"extent\"\n            value='[{\"name\":\"Europe\",\"srid\":4326,\"extent\":[[-11.25,35.25],[33.75,71.25]]},{\"name\":\"Switzerland\",\"srid\":2056,\"extent\":[[2485071.58,1074261.72],[2837119.8,1299941.79]]},{\"name\":\"Zurich\",\"srid\":2056,\"extent\":[[2668987.22976105,1223521.89852693],[2717254.80509207,1283848.12907045]]}]'\n          />\n        </td>\n        <td></td>\n      </tr>\n\n      <tr>\n        <td rowspan=\"3\"></td>\n        <td><label for=\"start-resolution-input\">Resolutions</label></td>\n        <td>\n          <input\n            id=\"start-resolution-input\"\n            title=\"resolution\"\n            placeholder=\"resolution\"\n            value=\"[[800,600],[1920,1080],[3840,2160]]\"\n          />\n        </td>\n        <td></td>\n      </tr>\n\n      <tr>\n        <td><label for=\"start-amount-input\">Amount</label></td>\n        <td>\n          <input\n            id=\"start-amount-input\"\n            type=\"number\"\n            min=\"0\"\n            step=\"1\"\n            value=\"3\"\n            title=\"amount\"\n            placeholder=\"amount\"\n          />\n        </td>\n        <td></td>\n      </tr>\n\n      <tr>\n        <td><label>Options</label></td>\n        <td>\n          <input type=\"checkbox\" id=\"option-show-results\" />\n          <label for=\"option-show-results\" class=\"label-left\"\n            >Show rendered images</label\n          >\n        </td>\n        <td></td>\n      </tr>\n\n      <tr>\n        <td></td>\n        <td colspan=\"2\"></td>\n      </tr>\n    </table>\n\n    <div id=\"results\"></div>\n\n    <script type=\"module\" src=\"src/index.ts\"></script>\n\n    <style>\n      body {\n        margin: 1em;\n        font-family: sans-serif;\n      }\n\n      button {\n        width: 15em;\n        text-align: left;\n      }\n\n      table {\n        border-spacing: 0.5em;\n      }\n      table td {\n        vertical-align: baseline;\n        border: 1px solid lightgrey;\n      }\n      table td.row-measure {\n        min-width: 12em;\n      }\n      table td.row-label {\n        min-width: 12em;\n      }\n      table td.row-input {\n        min-width: 20em;\n      }\n\n      label,\n      input,\n      select,\n      option {\n        box-sizing: border-box;\n        font-family: monospace;\n      }\n      select,\n      input:not([type=\"checkbox\"]) {\n        width: 100%;\n      }\n      label {\n        float: right;\n      }\n      label.label-left {\n        float: initial;\n      }\n\n      input::placeholder {\n        font-weight: bold;\n        opacity: 0.5;\n        color: red;\n      }\n\n      #results table {\n        margin-top: 2em;\n        margin-left: 2em;\n        border-collapse: collapse;\n      }\n      #results table td {\n        padding: 0.5em;\n        border: 1px solid lightgrey;\n        line-height: 2em;\n      }\n      #results span {\n        font-size: xx-small;\n      }\n\n      #results input {\n        vertical-align: bottom;\n        background-color: lightgreen;\n      }\n      #results input:not([value]) {\n        background-color: lightcoral;\n      }\n      #results input:not([value]).active {\n        background-color: lightseagreen;\n      }\n\n      #results img {\n        margin-left: 1em;\n        vertical-align: bottom;\n        width: 1.2em;\n        height: 1.2em;\n        object-fit: cover;\n        cursor: pointer;\n        border: 1px solid lightgrey;\n      }\n\n      #loading {\n        float: right;\n        width: 1em;\n        height: 1em;\n        border: 3px solid rgba(255, 255, 255, 0.3);\n        border-radius: 50%;\n        border-top-color: darkgrey;\n        animation: spin 1s ease-in-out infinite;\n        -webkit-animation: spin 1s ease-in-out infinite;\n      }\n\n      @keyframes spin {\n        to {\n          -webkit-transform: rotate(360deg);\n        }\n      }\n      @-webkit-keyframes spin {\n        to {\n          -webkit-transform: rotate(360deg);\n        }\n      }\n    </style>\n  </body>\n</html>\n"
  },
  {
    "path": "sites/performance/package.json",
    "content": "{\n  \"name\": \"@qgis-js/performance\",\n  \"version\": \"4.0.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite dev\",\n    \"build\": \"vite build\",\n    \"preview\": \"vite preview\",\n    \"perf\": \"playwright test\"\n  },\n  \"dependencies\": {\n    \"@qgis-js/utils\": \"workspace:*\",\n    \"qgis-js\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"@playwright/test\": \"^1.58.2\",\n    \"@types/node\": \"^22.19.15\",\n    \"typescript\": \"5.8.2\",\n    \"vite\": \"8.0.0\",\n    \"vite-plugin-static-copy\": \"3.3.0\"\n  }\n}\n"
  },
  {
    "path": "sites/performance/playwright.config.ts",
    "content": "import { defineConfig, devices } from \"@playwright/test\";\n\nexport default defineConfig({\n  testDir: \"./tests\",\n  retries: 0,\n  fullyParallel: false,\n  workers: 1,\n  reporter: [[\"list\"], [\"json\", { outputFile: \"test-results.json\" }], [\"html\"]],\n  use: {\n    baseURL: \"http://127.0.0.1:3000\",\n\n    /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */\n    trace: \"on-first-retry\",\n  },\n  projects: [\n    {\n      name: \"chromium\",\n      use: { ...devices[\"Desktop Chrome\"] },\n    },\n\n    {\n      name: \"firefox\",\n      use: { ...devices[\"Desktop Firefox\"] },\n    },\n  ],\n  webServer: {\n    command: \"npm run dev\",\n    url: \"http://127.0.0.1:3000\",\n    stdout: \"pipe\",\n    stderr: \"pipe\",\n  },\n});\n"
  },
  {
    "path": "sites/performance/report.html",
    "content": "<html>\n  <head>\n    <title>Performance Report</title>\n    <style>\n      body {\n        margin: 1em;\n        font-family: sans-serif;\n      }\n\n      #results table {\n        margin-top: 2em;\n        border-collapse: collapse;\n      }\n      #results table td {\n        padding: 0.2em;\n        border: 1px solid lightgrey;\n      }\n    </style>\n  </head>\n  <body>\n    <h1>Performance Report</h1>\n    <div id=\"results\">\n      <table>\n        <tr>\n          <th>Task</th>\n          <th>Project</th>\n          <th>Baseline</th>\n        </tr>\n        <tr>\n          <td>Load project</td>\n          <td><input id=\"project-measure\" type=\"number\" disabled /></td>\n          <td><input id=\"project-measure\" type=\"number\" disabled /></td>\n        </tr>\n        <tr>\n          <td>Load project</td>\n          <td><input id=\"project-measure\" type=\"number\" disabled /></td>\n          <td><input id=\"project-measure\" type=\"number\" disabled /></td>\n        </tr>\n        <tr>\n          <td>Load project</td>\n          <td><input id=\"project-measure\" type=\"number\" disabled /></td>\n          <td><input id=\"project-measure\" type=\"number\" disabled /></td>\n        </tr>\n      </table>\n    </div>\n  </body>\n</html>\n"
  },
  {
    "path": "sites/performance/src/index.ts",
    "content": "import { qgis, QGIS_JS_VERSION } from \"qgis-js\";\nimport type { QgisApi, EmscriptenFS } from \"qgis-js\";\n\nimport { useProjects } from \"@qgis-js/utils\";\nimport type { Project } from \"@qgis-js/utils\";\n\nlet API: QgisApi;\nlet FS: EmscriptenFS;\n\nconst qgisJsDemoProjects = (path: string) => ({\n  owner: \"boardend\",\n  repo: \"qgis-js-projects\",\n  path: \"/\" + path,\n  branch: \"main\",\n  prefix: `${path[0].toUpperCase()}${path.slice(1)}: `,\n});\n\nconst GITHUB_REPOS: Array<{\n  owner: string;\n  repo: string;\n  path?: string;\n  branch?: string;\n  prefix?: string;\n}> = [qgisJsDemoProjects(\"performance\")];\n\nconst PROJECTS = new Map<string, () => Project | Promise<Project>>();\nlet OPEN_PROJECT: any;\n\nasync function init() {\n  // add option to boot input select\n  const bootInput = document.querySelector(\"#boot-input\") as HTMLSelectElement;\n  // switching the Runtime is not supported yet\n  bootInput.disabled = true;\n\n  const option = document.createElement(\"option\");\n  option.value = \"bundle\";\n  option.textContent = `Bundle (${QGIS_JS_VERSION})`;\n  bootInput.appendChild(option);\n\n  const response = await fetch(\n    \"https://api.github.com/repos/qgis/qgis-js/releases\",\n  );\n  const json = await response.json();\n\n  for (const release of json) {\n    const option = document.createElement(\"option\");\n    option.value = release.tag_name;\n    option.textContent = release.tag_name;\n    bootInput.appendChild(option);\n  }\n\n  const btnBoot = document.querySelector(\"#boot\");\n  if (btnBoot) {\n    btnBoot.removeAttribute(\"disabled\");\n  }\n}\nasync function bootRuntime(_eClick: Event) {\n  let qgisLoader;\n  const runtime: string = (\n    document.querySelector(\"#boot-input\") as HTMLInputElement\n  ).value;\n  if (runtime === \"bundle\") {\n    qgisLoader = qgis;\n  } else {\n    //TODO respect runtime selection and load npm packages from CDN\n    /*\n    //see https://github.com/emscripten-core/emscripten/issues/8338\n    const { qgis } = await import(\n      \"https://cdn.jsdelivr.net/npm/qgis-js@0.0.1/dist/qgis.js\"\n    );\n    */\n    qgisLoader = qgis;\n  }\n\n  measureStart(\"boot\");\n  const { api, fs } = await qgisLoader({\n    prefix: \"/assets/wasm\",\n  });\n  measureEnd(\"boot\");\n\n  API = api;\n  FS = fs;\n\n  const btnTest = document.querySelector(\"#project\");\n  if (btnTest) {\n    btnTest.removeAttribute(\"disabled\");\n  }\n\n  const { openProject, loadRemoteProjects, loadGithubProjects } = useProjects(\n    FS,\n    (project: string) => {\n      measureStart(\"project\");\n      API.loadProject(project);\n      measureEnd(\"project\");\n\n      const btnFrame = document.querySelector(\"#frame\");\n      if (btnFrame) {\n        btnFrame.removeAttribute(\"disabled\");\n      }\n    },\n  );\n\n  OPEN_PROJECT = openProject;\n\n  const remoteProjects = await loadRemoteProjects();\n  remoteProjects.forEach((project) =>\n    PROJECTS.set(project.name, () => project),\n  );\n\n  for (const repo of GITHUB_REPOS) {\n    try {\n      const githubProjects = await loadGithubProjects(\n        repo.owner,\n        repo.repo,\n        repo.path,\n        repo.branch,\n      );\n      Object.entries(githubProjects).forEach(([name, projectLoadPromise]) => {\n        PROJECTS.set(name, projectLoadPromise);\n      });\n    } catch (error) {\n      console.warn(\n        `Unable to load GitHub project \"${repo.owner}/${repo.repo}\"`,\n        error,\n      );\n    }\n  }\n\n  const projectInput = document.querySelector(\n    \"#project-input\",\n  ) as HTMLInputElement;\n  for (const project of PROJECTS.keys()) {\n    const option = document.createElement(\"option\");\n    option.value = project;\n    option.textContent = project;\n    projectInput.appendChild(option);\n  }\n  projectInput.disabled = false;\n}\n\nasync function loadProject(_eClick: Event) {\n  const project = (document.querySelector(\"#project-input\") as HTMLInputElement)\n    .value;\n  if (!project) {\n    throw new Error(\"Project not found\");\n  }\n\n  const selectedProject = PROJECTS.get(project)!;\n  OPEN_PROJECT(selectedProject());\n}\n\nasync function renderFirstFrame(_eClick: Event) {\n  const [width, height] = JSON.parse(\n    (document.querySelector(\"#frame-input\") as HTMLInputElement).value,\n  );\n  if (!width || !height) {\n    throw new Error(\"Could not determine width and height\");\n  }\n\n  measureStart(\"frame\");\n  await API.renderImage(API.srid(), API.fullExtent(), width, height, 1);\n  measureEnd(\"frame\");\n\n  const btnStart = document.querySelector(\"#start\");\n  if (btnStart) {\n    btnStart.removeAttribute(\"disabled\");\n  }\n}\n\nasync function startPerformanceTest(_eClick: Event) {\n  const loader = document.createElement(\"div\");\n  loader.id = \"loading\";\n  (\n    _eClick.target as HTMLElement\n  ).parentElement?.parentElement?.firstElementChild?.appendChild(loader);\n\n  const optionShowResults = document.querySelector(\n    \"#option-show-results\",\n  ) as HTMLInputElement;\n\n  const extents = JSON.parse(\n    (document.querySelector(\"#start-extent-input\") as HTMLInputElement).value,\n  );\n  if (!extents || !Array.isArray(extents) || extents.length === 0) {\n    throw new Error(\"Invalid extent\");\n  }\n\n  const resolutions = JSON.parse(\n    (document.querySelector(\"#start-resolution-input\") as HTMLInputElement)\n      .value,\n  );\n  if (!resolutions || !Array.isArray(resolutions) || resolutions.length === 0) {\n    throw new Error(\"Invalid resolution\");\n  }\n\n  const amount = (\n    document.querySelector(\"#start-amount-input\") as HTMLInputElement\n  ).valueAsNumber;\n  const results = document.querySelector(\"#results\");\n  const table = document.createElement(\"table\");\n\n  // header\n  const row = document.createElement(\"tr\");\n  const cell = document.createElement(\"td\");\n  row.appendChild(cell);\n  for (const resolution of resolutions) {\n    const cell = document.createElement(\"td\");\n    const [width, height] = resolution;\n    cell.textContent = `${width}x${height}`;\n    row.appendChild(cell);\n  }\n  table.appendChild(row);\n\n  for (const extnet of extents) {\n    const row = document.createElement(\"tr\");\n    const cell = document.createElement(\"td\");\n\n    const title = document.createElement(\"strong\");\n    title.textContent = extnet.name;\n    cell.appendChild(title);\n    cell.appendChild(document.createTextNode(\"\\u00A0\"));\n    const srid = document.createElement(\"span\");\n    srid.textContent = ` (${extnet.srid})`;\n    cell.appendChild(srid);\n    const br = document.createElement(\"br\");\n    cell.appendChild(br);\n    const bbox = document.createElement(\"span\");\n    bbox.textContent =\n      extnet.extent[0].join(\"/\") + \", \" + extnet.extent[1].join(\"/\");\n    cell.appendChild(bbox);\n\n    row.appendChild(cell);\n    for (const resolution of resolutions) {\n      const cell = document.createElement(\"td\");\n      cell.classList.add(\"measure\");\n\n      for (let i = 0; i < amount; i++) {\n        const input = document.createElement(\"input\");\n        input.type = \"text\";\n        input.id = `measure-${extnet.name}-${resolution[0]}x${resolution[1]}-${i}`;\n        input.dataset.extent = JSON.stringify(extnet.extent);\n        input.dataset.resolution = JSON.stringify(resolution);\n        input.dataset.amount = i.toString();\n        input.disabled = true;\n        cell.appendChild(input);\n        cell.appendChild(document.createElement(\"br\"));\n      }\n\n      row.appendChild(cell);\n    }\n    table.appendChild(row);\n  }\n  results?.appendChild(table);\n\n  const todoSelector = \"#results td.measure > input:not([value])\";\n  while (document.querySelector(todoSelector)) {\n    // find all open inputs\n    const potentialJobs = Array.from(document.querySelectorAll(todoSelector));\n    const randomJob = potentialJobs[\n      Math.floor(Math.random() * potentialJobs.length)\n    ] as HTMLInputElement;\n    // get first input of this measure with input:not([value])\n    const job = randomJob.parentElement?.querySelector(\n      \"input:not([value])\",\n    ) as HTMLInputElement;\n\n    job.classList.add(\"active\");\n\n    const extent = JSON.parse(job.dataset.extent!);\n    const resolution = JSON.parse(job.dataset.resolution!);\n\n    measureStart(job.id);\n    const result = await API.renderImage(\n      API.srid(),\n      new API.QgsRectangle(\n        extent[0][0],\n        extent[0][1],\n        extent[1][0],\n        extent[1][1],\n      ),\n      resolution[0],\n      resolution[1],\n      1,\n    );\n\n    const time = measureEnd(job.id);\n    job.setAttribute(\"value\", \"\" + time);\n\n    if (optionShowResults.checked) {\n      // render the img to the DOM\n      const canvas = new OffscreenCanvas(result.width, result.height);\n      const context = canvas.getContext(\"2d\");\n      context!.putImageData(result, 0, 0);\n      const img = document.createElement(\"img\");\n      img.src = URL.createObjectURL(await canvas.convertToBlob());\n      job.insertAdjacentElement(\"afterend\", img);\n      img.addEventListener(\"click\", () => {\n        // open image in a new tab\n        const newTab = window.open();\n        newTab?.document.write(`<img src=\"${img.src}\" />`);\n      });\n    }\n\n    job.classList.remove(\"active\");\n  }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", (_e) => {\n  document\n    .querySelector(\"#boot\")\n    ?.addEventListener(\"click\", tryCatch(bootRuntime));\n  document\n    .querySelector(\"#project\")\n    ?.addEventListener(\"click\", tryCatch(loadProject));\n  document\n    .querySelector(\"#frame\")\n    ?.addEventListener(\"click\", tryCatch(renderFirstFrame));\n  document\n    .querySelector(\"#start\")\n    ?.addEventListener(\"click\", tryCatch(startPerformanceTest));\n\n  init();\n});\n\n// Utility functions\n\nfunction measureStart(name: string) {\n  performance.mark(`${name}-start`);\n}\n\nfunction measureEnd(name: string) {\n  performance.mark(`${name}-end`);\n  const measurement = performance.measure(\n    `${name}-measure`,\n    `${name}-start`,\n    `${name}-end`,\n  );\n  const element = document.querySelector(`#${name}-measure`);\n  if (element) {\n    (element as HTMLInputElement).valueAsNumber = measurement.duration;\n  }\n  return measurement.duration;\n}\n\nfunction tryCatch(handler: (event: Event) => void) {\n  return async (event: Event) => {\n    try {\n      await handler(event);\n    } catch (error: any) {\n      reportError(error);\n      throw error;\n    }\n  };\n}\n\nfunction reportError(error: any) {\n  document.body.style.backgroundColor = \"#ffcccc\";\n  const errorDiv = document.createElement(\"div\");\n  errorDiv.style.marginTop = \"2em\";\n  errorDiv.style.color = \"red\";\n  errorDiv.textContent = error.message;\n  document.body.appendChild(errorDiv);\n}\n"
  },
  {
    "path": "sites/performance/tests/performance.spec.ts",
    "content": "import { test, expect } from \"@playwright/test\";\n\nimport { join, dirname } from \"path\";\nimport { readdirSync } from \"fs\";\nimport { fileURLToPath } from \"url\";\n\nimport { createRequire } from \"module\";\nconst require = createRequire(import.meta.url);\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\ntest.describe.serial(\"Performance measurement\", () => {\n  // reading test projects\n  const projectsDir = join(__dirname, \"..\", \"public\", \"projects\");\n  const projectNames = readdirSync(projectsDir);\n\n  // create a test for each project\n  for (const projectName of projectNames) {\n    console.log(projectName);\n\n    const json = require(`../public/projects/${projectName}/measure.json`);\n    console.dir(json);\n\n    test(`Test project \"${projectName}\"`, async ({ page }) => {\n      await page.goto(\"/\");\n\n      await expect(page).toHaveTitle(/^qgis-js-performance$/);\n\n      const btnBoot = page.locator(\"#boot\");\n      const btnProject = page.locator(\"#project\");\n      const btnFrame = page.locator(\"#frame\");\n      const btnStart = page.locator(\"#start\");\n\n      await test.step(\"Boot runtime\", async () => {\n        expect(btnBoot).not.toBeNull();\n        await btnBoot?.click();\n        await expect(btnProject).toBeEnabled();\n      });\n\n      await test.step(\"Load project\", async () => {\n        expect(btnProject).not.toBeNull();\n\n        const inpProject = page.locator(\"#project-input\");\n        expect(btnProject).not.toBeNull();\n        await inpProject.fill(projectName);\n\n        await btnProject?.click();\n\n        await expect(btnFrame).toBeEnabled({ timeout: 10000 });\n      });\n\n      await test.step(\"Render first frame\", async () => {\n        expect(btnFrame).not.toBeNull();\n\n        await btnFrame?.click();\n\n        await expect(btnStart).toBeEnabled({ timeout: 10000 });\n      });\n\n      await test.step(\"Run performance measurement\", async () => {\n        expect(btnStart).not.toBeNull();\n\n        const inpExtent = page.locator(\"#start-extent-input\");\n        expect(btnProject).not.toBeNull();\n        await inpExtent.fill(JSON.stringify(json.extent, undefined, \"\"));\n\n        const inpResolution = page.locator(\"#start-resolution-input\");\n        expect(inpResolution).not.toBeNull();\n        await inpResolution.fill(\n          JSON.stringify(json.resolution, undefined, \"\"),\n        );\n\n        await btnStart?.click();\n\n        await page.waitForSelector(\"#results\");\n\n        // wait until div with id \"loader\" is gone\n        await page.waitForSelector(\"#loading\", { state: \"detached\" });\n      });\n\n      console.log(\"test done\");\n    });\n  }\n});\n"
  },
  {
    "path": "sites/performance/tsconfig.json",
    "content": "{\n  \"extends\": \"../../tsconfig.json\",\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "sites/performance/vite.config.ts",
    "content": "import { defineConfig } from \"vite\";\n\nimport QgisRuntimePlugin from \"../../build/vite/QgisRuntimePlugin\";\nimport DirectoryListingPlugin from \"../../build/vite/DirectoryListingPlugin\";\nimport CrossOriginIsolationPlugin, {\n  CrossOriginIsolationResponseHeaders,\n} from \"../../build/vite/CrossOriginIsolationPlugin\";\n\nimport packageJson from \"./package.json\";\n\nexport default defineConfig({\n  define: {\n    __QGIS_JS_VERSION: JSON.stringify(packageJson.version),\n  },\n  server: {\n    port: 3000,\n  },\n  preview: {\n    headers: {\n      ...CrossOriginIsolationResponseHeaders,\n    },\n  },\n  plugins: [\n    QgisRuntimePlugin({\n      name: \"qgis-js\",\n      outputDir: \"build/wasm\",\n    }),\n    CrossOriginIsolationPlugin(),\n    DirectoryListingPlugin([\"public/projects\"]),\n  ],\n});\n"
  },
  {
    "path": "src/api/QgisApi.cpp",
    "content": "#include <memory>\n#include <optional>\n#include <string>\n\n#include <qgsexpressioncontextutils.h>\n#include <qgslayerdefinition.h>\n#include <qgslayertree.h>\n#include <qgslayertreemodel.h>\n#include <qgsmaprenderercustompainterjob.h>\n#include <qgsmaprendererparalleljob.h>\n#include <qgsmaprenderersequentialjob.h>\n#include <qgsmapsettings.h>\n#include <qgsproject.h>\n#include <qgstiles.h>\n\n#include <QImage>\n#include <QString>\n#include <QtConcurrent/QtConcurrent>\n\n#include \"../model/QgsLayerTreeLayer.hpp\"\n#include \"../model/QgsMapRendererJob.hpp\"\n#include \"../model/QgsMapRendererParallelJob.hpp\"\n#include \"../model/QgsMapRendererQImageJob.hpp\"\n#include \"../model/QgsPointXY.hpp\"\n#include \"../model/QgsRectangle.hpp\"\n\n#include <emscripten/bind.h>\n#include <emscripten/val.h>\n\nQList<QgsMapLayer *> QgisApi_allLayers() {\n  return QgsProject::instance()->layerTreeRoot()->layerOrder();\n}\n\nQList<QgsMapLayer *> QgisApi_visibleLayers() {\n  QList<QgsMapLayer *> result = {};\n  auto root = QgsProject::instance()->layerTreeRoot();\n  const QList<QgsMapLayer *> allLayers = root->layerOrder();\n  for (QgsMapLayer *layer : allLayers) {\n    QgsLayerTreeLayer *nodeLayer = root->findLayer(layer->id());\n    if (nodeLayer && nodeLayer->layer()->isSpatial() && nodeLayer->isVisible()) {\n      result << layer;\n    }\n  }\n  return result;\n}\n\nQList<QgsMapLayer *> resolveLayers(std::optional<emscripten::val> layerIds) {\n  if (!layerIds.has_value() || layerIds->isUndefined() || layerIds->isNull())\n    return QgisApi_visibleLayers();\n  QList<QgsMapLayer *> result;\n  int length = (*layerIds)[\"length\"].as<int>();\n  for (int i = 0; i < length; i++) {\n    std::string id = (*layerIds)[i].as<std::string>();\n    QgsMapLayer *layer = QgsProject::instance()->mapLayer(QString::fromStdString(id));\n    if (layer) result << layer;\n  }\n  return result;\n}\n\nbool QgisApi_loadProject(std::string filename) {\n  Qgis::ProjectReadFlags readFlags =\n    Qgis::ProjectReadFlag::ForceReadOnlyLayers | Qgis::ProjectReadFlag::TrustLayerMetadata;\n\n  bool res = QgsProject::instance()->read(QString::fromStdString(filename), readFlags);\n  if (!res) return false;\n\n  return true;\n}\n\nQgsRectangle QgisApi_fullExtent(std::optional<emscripten::val> layerIds) {\n  QgsMapSettings mapSettings;\n  mapSettings.setDestinationCrs(QgsProject::instance()->crs());\n  mapSettings.setLayers(resolveLayers(layerIds));\n  return mapSettings.fullExtent();\n}\n\nstd::string QgisApi_srid() {\n  return QgsProject::instance()->crs().authid().toStdString();\n}\n\nvoid QgisApi_renderXYZTile(\n  unsigned long x,\n  unsigned long y,\n  unsigned int z,\n  unsigned int tileSize,\n  float pixelRatio,\n  float extentBufferFactor,\n  emscripten::val callback,\n  std::optional<emscripten::val> layerIds) {\n\n  QgsMapSettings mapSettings;\n\n  mapSettings.setOutputImageFormat(QImage::Format_ARGB32);\n  mapSettings.setBackgroundColor(Qt::transparent);\n  mapSettings.setOutputSize(QSize(tileSize, tileSize));\n  mapSettings.setOutputDpi(96.0 * pixelRatio);\n\n  mapSettings.setLayers(resolveLayers(layerIds));\n\n  mapSettings.setDestinationCrs(QgsCoordinateReferenceSystem(QStringLiteral(\"EPSG:3857\")));\n\n  QgsTileMatrix mTileMatrix = QgsTileMatrix::fromWebMercator(z);\n  auto extent = mTileMatrix.tileExtent(QgsTileXYZ(x, y, z));\n  mapSettings.setExtent(extent);\n\n  auto tileExtentBuffer = extent.width() * extentBufferFactor;\n  if (tileExtentBuffer > 0.0) {\n    mapSettings.setExtentBuffer(tileExtentBuffer);\n  }\n\n  mapSettings.setFlag(Qgis::MapSettingsFlag::RenderMapTile, true);\n\n  QgsExpressionContext context = QgsProject::instance()->createExpressionContext();\n  context << QgsExpressionContextUtils::mapSettingsScope(mapSettings);\n  mapSettings.setExpressionContext(context);\n\n  mapSettings.setPathResolver(QgsProject::instance()->pathResolver());\n\n  QgsMapRendererSequentialJob *job = new QgsMapRendererSequentialJob(mapSettings);\n  QObject::connect(job, &QgsMapRendererSequentialJob::finished, [job, callback] {\n    auto image = job->renderedImage();\n    image.rgbSwap(); // for html canvas\n    callback(emscripten::val(emscripten::typed_memory_view(\n      image.width() * image.height() * 4, (const unsigned char *)image.constBits())));\n    job->deleteLater();\n  });\n  job->start();\n}\n\nvoid QgisApi_renderImage(\n  std::string srid,\n  const QgsRectangle &extent,\n  unsigned int width,\n  unsigned int height,\n  float pixelRatio,\n  emscripten::val callback,\n  std::optional<emscripten::val> layerIds) {\n\n  QgsMapSettings mapSettings;\n\n  mapSettings.setOutputImageFormat(QImage::Format_ARGB32);\n  mapSettings.setBackgroundColor(Qt::transparent);\n  mapSettings.setOutputSize(QSize(width, height));\n  mapSettings.setOutputDpi(96.0 * pixelRatio);\n\n  mapSettings.setLayers(resolveLayers(layerIds));\n\n  mapSettings.setDestinationCrs(QgsCoordinateReferenceSystem(QString::fromStdString(srid)));\n\n  mapSettings.setExtent(extent);\n\n  QgsExpressionContext context = QgsProject::instance()->createExpressionContext();\n  context << QgsExpressionContextUtils::mapSettingsScope(mapSettings);\n  mapSettings.setExpressionContext(context);\n\n  mapSettings.setPathResolver(QgsProject::instance()->pathResolver());\n\n  // START optimizations\n  QgsVectorSimplifyMethod simplify;\n  simplify.setSimplifyHints(Qgis::VectorRenderingSimplificationFlag::FullSimplification);\n  mapSettings.setSimplifyMethod(simplify);\n\n  mapSettings.setFlag(Qgis::MapSettingsFlag::UseRenderingOptimization, true);\n  mapSettings.setFlag(Qgis::MapSettingsFlag::ForceRasterMasks, true);\n  mapSettings.setFlag(Qgis::MapSettingsFlag::RenderPreviewJob, true);\n\n  mapSettings.setRendererUsage(Qgis::RendererUsage::View);\n  // END optimizations\n\n  QgsMapRendererParallelJob *job = new QgsMapRendererParallelJob(mapSettings);\n\n  QObject::connect(job, &QgsMapRendererParallelJob::finished, [job, callback] {\n    auto image = job->renderedImage();\n    image.rgbSwap(); // for html canvas\n    callback(emscripten::val(emscripten::typed_memory_view(\n      image.width() * image.height() * 4, (const unsigned char *)image.constBits())));\n    job->deleteLater();\n  });\n\n  job->start();\n}\n\nQgsMapRendererParallelJob *QgisApi_renderJob(\n  std::string srid,\n  const QgsRectangle &extent,\n  unsigned int width,\n  unsigned int height,\n  float pixelRatio,\n  std::optional<emscripten::val> layerIds) {\n\n  QgsMapSettings mapSettings;\n\n  mapSettings.setOutputImageFormat(QImage::Format_ARGB32);\n  mapSettings.setBackgroundColor(Qt::transparent);\n  mapSettings.setOutputSize(QSize(width, height));\n  mapSettings.setOutputDpi(96.0 * pixelRatio);\n\n  mapSettings.setLayers(resolveLayers(layerIds));\n\n  mapSettings.setDestinationCrs(QgsCoordinateReferenceSystem(QString::fromStdString(srid)));\n\n  mapSettings.setExtent(extent);\n\n  QgsExpressionContext context = QgsProject::instance()->createExpressionContext();\n  context << QgsExpressionContextUtils::mapSettingsScope(mapSettings);\n  mapSettings.setExpressionContext(context);\n\n  mapSettings.setPathResolver(QgsProject::instance()->pathResolver());\n\n  // START optimizations\n  QgsVectorSimplifyMethod simplify;\n  simplify.setSimplifyHints(Qgis::VectorRenderingSimplificationFlag::FullSimplification);\n  mapSettings.setSimplifyMethod(simplify);\n\n  mapSettings.setFlag(Qgis::MapSettingsFlag::UseRenderingOptimization, true);\n  mapSettings.setFlag(Qgis::MapSettingsFlag::ForceRasterMasks, true);\n  mapSettings.setFlag(Qgis::MapSettingsFlag::RenderPreviewJob, true);\n  mapSettings.setFlag(Qgis::MapSettingsFlag::RenderPartialOutput, true);\n\n  mapSettings.setRendererUsage(Qgis::RendererUsage::View);\n  // END optimizations\n\n  QgsMapRendererParallelJob *job = new QgsMapRendererParallelJob(mapSettings);\n\n  job->start();\n\n  return job;\n}\n\nconst QgsRectangle QgisApi_transformRectangle(\n  const QgsRectangle &inputRectangle, std::string inputSrid, std::string outputSrid) {\n  QgsCoordinateTransform transform(\n    QgsCoordinateReferenceSystem(QString::fromStdString(inputSrid)),\n    QgsCoordinateReferenceSystem(QString::fromStdString(outputSrid)),\n    QgsProject::instance());\n  return transform.transformBoundingBox(inputRectangle);\n}\n\nLayerTreeGroup QgisApi_layerTreeRoot() {\n  return LayerTreeGroup(QgsProject::instance()->layerTreeRoot());\n}\n\nconst std::vector<std::string> QgisApi_mapThemes() {\n  std::vector<std::string> result = {};\n  for (const QString &theme : QgsProject::instance()->mapThemeCollection()->mapThemes()) {\n    result.push_back(theme.toStdString());\n  }\n  return result;\n}\n\nconst std::string QgisApi_getMapTheme() {\n  QgsLayerTree *layerTreeRoot = QgsProject::instance()->layerTreeRoot();\n  QgsMapThemeCollection *collection = QgsProject::instance()->mapThemeCollection();\n  QgsLayerTreeModel model(layerTreeRoot);\n  auto currentState = QgsMapThemeCollection::createThemeFromCurrentState(layerTreeRoot, &model);\n  for (const QString &theme : QgsProject::instance()->mapThemeCollection()->mapThemes()) {\n    if (currentState == QgsProject::instance()->mapThemeCollection()->mapThemeState(theme)) {\n      return theme.toStdString();\n    }\n  }\n  return \"\";\n}\n\nconst bool QgisApi_setMapTheme(std::string themeName) {\n  QString qThemeName = QString::fromStdString(themeName);\n  if (!QgsProject::instance()->mapThemeCollection()->hasMapTheme(qThemeName)) {\n    return false;\n  } else {\n    QgsLayerTree *layerTreeRoot = QgsProject::instance()->layerTreeRoot();\n    QgsMapThemeCollection *collection = QgsProject::instance()->mapThemeCollection();\n    QgsLayerTreeModel model(layerTreeRoot);\n    collection->applyTheme(qThemeName, layerTreeRoot, &model);\n    return true;\n  }\n}\n\nemscripten::val QgisApi_globalVariables() {\n  emscripten::val result = emscripten::val::object();\n  std::unique_ptr<QgsExpressionContextScope> scope(QgsExpressionContextUtils::globalScope());\n  for (const QString &name : scope->variableNames()) {\n    result.set(name.toStdString(), scope->variable(name).toString().toStdString());\n  }\n  return result;\n}\n\nemscripten::val QgisApi_projectVariables() {\n  emscripten::val result = emscripten::val::object();\n  std::unique_ptr<QgsExpressionContextScope> scope(\n    QgsExpressionContextUtils::projectScope(QgsProject::instance()));\n  for (const QString &name : scope->variableNames()) {\n    result.set(name.toStdString(), scope->variable(name).toString().toStdString());\n  }\n  return result;\n}\n\nvoid QgisApi_setGlobalVariables(emscripten::val variables) {\n  QVariantMap vars;\n  emscripten::val keys = emscripten::val::global(\"Object\").call<emscripten::val>(\"keys\", variables);\n  int length = keys[\"length\"].as<int>();\n  for (int i = 0; i < length; i++) {\n    std::string key = keys[i].as<std::string>();\n    std::string value = variables[key].as<std::string>();\n    vars.insert(QString::fromStdString(key), QString::fromStdString(value));\n  }\n  QgsExpressionContextUtils::setGlobalVariables(vars);\n}\n\nvoid QgisApi_setProjectVariables(emscripten::val variables) {\n  QVariantMap vars;\n  emscripten::val keys = emscripten::val::global(\"Object\").call<emscripten::val>(\"keys\", variables);\n  int length = keys[\"length\"].as<int>();\n  for (int i = 0; i < length; i++) {\n    std::string key = keys[i].as<std::string>();\n    std::string value = variables[key].as<std::string>();\n    vars.insert(QString::fromStdString(key), QString::fromStdString(value));\n  }\n  QgsExpressionContextUtils::setProjectVariables(QgsProject::instance(), vars);\n}\n\nstd::string QgisApi_renderLegend(float dpi, std::optional<emscripten::val> layerIds) {\n  if (!layerIds.has_value() || layerIds->isUndefined() || layerIds->isNull()) {\n    return renderLegendForTree(QgsProject::instance()->layerTreeRoot(), dpi);\n  }\n  QgsLayerTree tempRoot;\n  int length = (*layerIds)[\"length\"].as<int>();\n  for (int i = 0; i < length; i++) {\n    std::string id = (*layerIds)[i].as<std::string>();\n    QgsMapLayer *layer = QgsProject::instance()->mapLayer(QString::fromStdString(id));\n    if (layer) tempRoot.addLayer(layer);\n  }\n  return renderLegendForTree(&tempRoot, dpi);\n}\n\nstruct LayerDefinitionResult {\n  bool success;\n  std::string errorMessage;\n};\n\nLayerDefinitionResult\nQgisApi_loadLayerDefinition(std::string path, std::optional<LayerTreeGroup> targetGroup) {\n  QString errorMessage;\n  QgsLayerTreeGroup *group = QgsProject::instance()->layerTreeRoot();\n  if (targetGroup.has_value() && targetGroup->isValid()) {\n    group = static_cast<QgsLayerTreeGroup *>(targetGroup->nativeNode());\n  }\n  bool ok = QgsLayerDefinition::loadLayerDefinition(\n    QString::fromStdString(path), QgsProject::instance(), group, errorMessage);\n  return {ok, errorMessage.toStdString()};\n}\n\nEMSCRIPTEN_BINDINGS(QgisApi) {\n  emscripten::value_object<LayerDefinitionResult>(\"LayerDefinitionResult\")\n    .field(\"success\", &LayerDefinitionResult::success)\n    .field(\"errorMessage\", &LayerDefinitionResult::errorMessage);\n  emscripten::function(\"loadProject\", &QgisApi_loadProject);\n  emscripten::function(\"fullExtent\", &QgisApi_fullExtent);\n  emscripten::function(\"srid\", &QgisApi_srid);\n  emscripten::function(\"renderImage\", &QgisApi_renderImage);\n  emscripten::function(\"renderXYZTile\", &QgisApi_renderXYZTile);\n  emscripten::function(\"renderJob\", &QgisApi_renderJob, emscripten::allow_raw_pointers());\n  emscripten::function(\"transformRectangle\", &QgisApi_transformRectangle);\n  emscripten::function(\"layerTreeRoot\", &QgisApi_layerTreeRoot);\n  emscripten::function(\"mapThemes\", &QgisApi_mapThemes);\n  emscripten::function(\"getMapTheme\", &QgisApi_getMapTheme);\n  emscripten::function(\"setMapTheme\", &QgisApi_setMapTheme);\n  emscripten::register_vector<std::string>(\"vector<std::string>\");\n  emscripten::register_optional<emscripten::val>();\n  emscripten::register_optional<LayerTreeGroup>();\n  emscripten::function(\"globalVariables\", &QgisApi_globalVariables);\n  emscripten::function(\"projectVariables\", &QgisApi_projectVariables);\n  emscripten::function(\"setGlobalVariables\", &QgisApi_setGlobalVariables);\n  emscripten::function(\"setProjectVariables\", &QgisApi_setProjectVariables);\n  emscripten::function(\"renderLegend\", &QgisApi_renderLegend);\n  emscripten::function(\"loadLayerDefinition\", &QgisApi_loadLayerDefinition);\n}\n"
  },
  {
    "path": "src/api/QgisApi.ts",
    "content": "import {\n  QgisModelConstructors,\n  QgsMapRendererParallelJob,\n  QgsRectangle,\n  QgsLayerTreeGroup,\n} from \"./QgisModel\";\n\nexport interface LayerDefinitionResult {\n  success: boolean;\n  errorMessage: string;\n}\n\n/**\n * Common QGIS API which exposes the {@link QgisModelConstructors} and methods that can be accessed from both the public {@link QgisApi} and the {@link InternalQgisApi}.\n */\nexport interface CommonQgisApi extends QgisModelConstructors {\n  /**\n   * Loads a QGIS project file.\n   *\n   * @param filename - The path to the QGIS project file.\n   * @returns true if the project was loaded successfully, false otherwise.\n   */\n  loadProject(filename: string): boolean;\n\n  /**\n   * Loads a QGIS Layer Definition (.qlr) file into the project.\n   *\n   * @param path - Path to the .qlr file on the virtual filesystem.\n   * @param targetGroup - Optional target group to load into. Defaults to the root group.\n   * @returns Result indicating success or failure with an error message.\n   */\n  loadLayerDefinition(\n    path: string,\n    targetGroup?: QgsLayerTreeGroup,\n  ): LayerDefinitionResult;\n\n  /**\n   * Returns the full extent of the loaded project.\n   *\n   * @returns The full extent of the loaded project.\n   */\n  fullExtent(layerIds?: string[]): QgsRectangle;\n\n  /**\n   * Returns the SRID of the loaded project.\n   *\n   * @returns The SRID of the loaded project.\n   */\n  srid(): string;\n\n  /**\n   * Transforms a rectangle from one SRID to another.\n   *\n   * @param rect - The rectangle to transform.\n   * @param inputSrid - The SRID of the input rectangle.\n   * @param outputSrid - The SRID of the output rectangle.\n   * @returns The transformed rectangle.\n   */\n  transformRectangle(\n    rect: QgsRectangle,\n    inputSrid: string,\n    outputSrid: string,\n  ): QgsRectangle;\n\n  /**\n   * Gets the current map theme of the current project.\n   *\n   * @returns The name of the current map theme. An empty string if no map theme is set.\n   */\n  getMapTheme(): string;\n\n  /**\n   * Sets a map theme of the current project.\n   *\n   * @param\n   * @returns true if the map theme was loaded successfully, false otherwise.\n   */\n  setMapTheme(mapTheme: string): boolean;\n\n  /**\n   * Returns the global expression variables.\n   *\n   * These are system-level variables such as `qgis_version`, `qgis_locale`,\n   * `qgis_os_name`, `qgis_platform`, etc.\n   *\n   * @returns A record of variable names to their string values.\n   */\n  globalVariables(): Record<string, string>;\n\n  /**\n   * Returns the project expression variables.\n   *\n   * These are project-level variables such as `project_crs`, `project_title`,\n   * `project_filename`, `layer_ids`, `project_area_units`, etc.\n   * Only available after a project has been loaded.\n   *\n   * @returns A record of variable names to their string values.\n   */\n  projectVariables(): Record<string, string>;\n\n  /**\n   * Sets global expression variables.\n   *\n   * Custom variables that will be available in all expression contexts.\n   * Note: This replaces all custom global variables.\n   *\n   * @param variables - A record of variable names to their string values.\n   */\n  setGlobalVariables(variables: Record<string, string>): void;\n\n  /**\n   * Sets project expression variables.\n   *\n   * Custom variables that will be available in the project's expression contexts.\n   * Note: This replaces all custom project variables.\n   *\n   * @param variables - A record of variable names to their string values.\n   */\n  setProjectVariables(variables: Record<string, string>): void;\n\n  /**\n   * Returns the root of the project's layer tree.\n   *\n   * @returns The root layer tree node.\n   */\n  layerTreeRoot(): QgsLayerTreeGroup;\n\n  /**\n   * Renders the full project legend as a PNG image.\n   *\n   * @param dpi - The DPI for rendering the legend.\n   * @returns A base64 data URL of the legend PNG, or an empty string if the legend is empty.\n   */\n  renderLegend(dpi: number, layerIds?: string[]): string;\n\n  /**\n   * Renders an image of the loaded project and provides a QgsMapRendererParallelJob object to monitor the rendering progress and to retrieve preview images.\n   *\n   * @param srid - The SRID of the image.\n   * @param extent - The extent of the image.\n   * @param width - The width of the image.\n   * @param height - The height of the image.\n   * @param pixelRatio - The optional pixel ratio of the image which defaults to 1.\n   * @returns A QgsMapRendererParallelJob object that can be used to monitor the rendering progress and to retrieve preview images.\n   */\n  renderJob(\n    srid: string,\n    extent: QgsRectangle,\n    width: number,\n    height: number,\n    pixelRatio: number,\n    layerIds?: string[],\n  ): QgsMapRendererParallelJob;\n}\n\n/**\n * The QgisApiAdapter provides convenience methods in addition to the CommonQgisApi.\n */\nexport interface QgisApiAdapter {\n  /**\n   * Renders an image of the loaded project.\n   *\n   * @param srid - The SRID of the image.\n   * @param extent - The extent of the image.\n   * @param width - The width of the image.\n   * @param height - The height of the image.\n   * @param pixelRatio - The optional pixel ratio of the image which defaults to 1.\n   * @returns The rendered tile as an ImageData object.\n   */\n  renderImage(\n    srid: string,\n    extent: QgsRectangle,\n    width: number,\n    height: number,\n    pixelRatio?: number,\n    layerIds?: string[],\n  ): Promise<ImageData>;\n\n  /**\n   * Renders a tile of the loaded project in the XYZ scheme (EPSG:3857).\n   *\n   * @param x - The x coordinate of the tile.\n   * @param y - The y coordinate of the tile.\n   * @param z - The z coordinate of the tile.\n   * @param tileSize - The optional size of the tile which defaults to 256.\n   * @param pixelRatio - The optional pixel ratio of the tile which defaults to 1.\n   * @param extentBufferFactor - The optional extent buffer factor of the tile which defaults to 0.\n   * @param layerIds - Optional array of layer IDs to render. If omitted, renders all visible layers.\n   * @returns The rendered tile as an ImageData object.\n   */\n  renderXYZTile(\n    x: number,\n    y: number,\n    z: number,\n    tileSize?: number,\n    pixelRatio?: number,\n    extentBufferFactor?: number,\n    layerIds?: string[],\n  ): Promise<ImageData>;\n\n  /**\n   * Returns the map themes of the loaded project.\n   *\n   * @returns The map themes of the loaded project.\n   */\n  mapThemes(): readonly string[];\n}\n\n/**\n * Interface representing the public QgisApi.\n */\nexport interface QgisApi extends CommonQgisApi, QgisApiAdapter {}\n\n/**\n * The internal Qgis API which can be accessed from the QgisRuntimeModule\n *\n * This interface is not a stable API and may change at any time. Use the public {@link QgisApi} instead.\n */\nexport interface InternalQgisApi extends CommonQgisApi {\n  renderImage(\n    srid: string,\n    extent: QgsRectangle,\n    width: number,\n    height: number,\n    pixelRatio: number,\n    callback: (tileData: ArrayBufferLike) => void,\n    layerIds?: string[],\n  ): void;\n  renderXYZTile(\n    x: number,\n    y: number,\n    z: number,\n    tileSize: number,\n    pixelRatio: number,\n    extentBuffer: number,\n    callback: (tileData: ArrayBufferLike) => void,\n    layerIds?: string[],\n  ): number;\n  mapThemes(): any;\n}\n"
  },
  {
    "path": "src/api/QgisModel.ts",
    "content": "import type { QgsPointXY, QgsPointXYConstructors } from \"../model/QgsPointXY\";\nimport type {\n  QgsRectangle,\n  QgsRectangleConstructors,\n} from \"../model/QgsRectangle\";\nimport type { QgsLayerTreeModelLegendNode } from \"../model/QgsLayerTreeModelLegendNode\";\nimport type { QgsLayerTreeNode } from \"../model/QgsLayerTreeNode\";\nimport { NodeType } from \"../model/QgsLayerTreeNode\";\nimport type { QgsLayerTreeGroup } from \"../model/QgsLayerTreeGroup\";\nimport type { QgsLayerTreeLayer } from \"../model/QgsLayerTreeLayer\";\nimport type { QgsMapLayer, QgsVectorLayer } from \"../model/QgsMapLayer\";\nimport { LayerType } from \"../model/QgsMapLayer\";\nimport type { QgsMapRendererParallelJob } from \"../model/QgsMapRendererParallelJob\";\nimport type { QgsMapRendererJob } from \"../model/QgsMapRendererJob\";\nimport type { QgsMapRendererQImageJob } from \"../model/QgsMapRendererQImageJob\";\n\nexport type {\n  QgsMapRendererJob,\n  QgsMapRendererQImageJob,\n  QgsMapRendererParallelJob,\n  QgsPointXY,\n  QgsRectangle,\n  QgsLayerTreeModelLegendNode,\n  QgsLayerTreeNode,\n  QgsLayerTreeGroup,\n  QgsLayerTreeLayer,\n  QgsMapLayer,\n  QgsVectorLayer,\n};\n\nexport type { LayerDefinitionResult } from \"./QgisApi\";\n\nexport { LayerType, NodeType };\n\n/* prettier-ignore */\n\nexport interface QgisModelConstructors\n  extends\n    QgsPointXYConstructors,\n    QgsRectangleConstructors\n  {}\n"
  },
  {
    "path": "src/model/QgsLayerTreeGroup.hpp",
    "content": "#pragma once\n\n#include <string>\n\n#include <qgslayertreegroup.h>\n\n#include \"./QgsLayerTreeModelLegendNode.hpp\"\n#include \"./QgsLayerTreeNode.hpp\"\n\nclass LayerTreeGroup : public LayerTreeNode {\npublic:\n  LayerTreeGroup() : LayerTreeNode(nullptr) {}\n  LayerTreeGroup(QgsLayerTreeGroup *group) : LayerTreeNode(group) {}\n\n  emscripten::val findLayers() const {\n    emscripten::val result = emscripten::val::array();\n    if (auto *group = asGroup()) {\n      for (QgsLayerTreeLayer *layerNode : group->findLayers()) {\n        result.call<void>(\"push\", wrapNode(layerNode));\n      }\n    }\n    return result;\n  }\n\n  emscripten::val findGroup(std::string name) const {\n    if (auto *group = asGroup()) {\n      QgsLayerTreeGroup *found = group->findGroup(QString::fromStdString(name));\n      if (found) return wrapNode(found);\n    }\n    return wrapNode(nullptr);\n  }\n\n  emscripten::val findLayer(std::string layerId) const {\n    if (auto *group = asGroup()) {\n      QgsLayerTreeLayer *found = group->findLayer(QString::fromStdString(layerId));\n      if (found) return wrapNode(found);\n    }\n    return wrapNode(nullptr);\n  }\n\n  bool isMutuallyExclusive() const {\n    if (auto *group = asGroup()) return group->isMutuallyExclusive();\n    return false;\n  }\n\n  void setIsMutuallyExclusive(bool exclusive) {\n    if (auto *group = asGroup()) group->setIsMutuallyExclusive(exclusive);\n  }\n\n  std::string renderLegend(float dpi) const {\n    auto *group = asGroup();\n    if (!group) return \"\";\n    QgsLayerTree tempRoot;\n    tempRoot.addChildNode(group->clone());\n    return renderLegendForTree(&tempRoot, dpi);\n  }\n\nprivate:\n  QgsLayerTreeGroup *asGroup() const {\n    Q_ASSERT(_node && _node->nodeType() == QgsLayerTreeNode::NodeGroup);\n    return static_cast<QgsLayerTreeGroup *>(_node);\n  }\n};\n\nEMSCRIPTEN_BINDINGS(QgsLayerTreeGroup) {\n  emscripten::class_<LayerTreeGroup, emscripten::base<LayerTreeNode>>(\"QgsLayerTreeGroup\")\n    .function(\"findLayers\", &LayerTreeGroup::findLayers)\n    .function(\"findGroup\", &LayerTreeGroup::findGroup)\n    .function(\"findLayer\", &LayerTreeGroup::findLayer)\n    .property(\n      \"isMutuallyExclusive\",\n      &LayerTreeGroup::isMutuallyExclusive,\n      &LayerTreeGroup::setIsMutuallyExclusive)\n    .function(\"renderLegend\", &LayerTreeGroup::renderLegend);\n}\n"
  },
  {
    "path": "src/model/QgsLayerTreeGroup.ts",
    "content": "import type { QgsLayerTreeNode } from \"./QgsLayerTreeNode\";\nimport type { QgsLayerTreeLayer } from \"./QgsLayerTreeLayer\";\n\nexport interface QgsLayerTreeGroup extends QgsLayerTreeNode {\n  isMutuallyExclusive: boolean;\n\n  findLayers(): QgsLayerTreeLayer[];\n  findGroup(name: string): QgsLayerTreeGroup | null;\n  findLayer(layerId: string): QgsLayerTreeLayer | null;\n  renderLegend(dpi: number): string;\n}\n"
  },
  {
    "path": "src/model/QgsLayerTreeLayer.hpp",
    "content": "#pragma once\n\n#include <string>\n\n#include <qgslayertree.h>\n#include <qgslayertreelayer.h>\n\n#include \"./QgsLayerTreeGroup.hpp\"\n#include \"./QgsMapLayer.hpp\"\n\nclass LayerTreeLayer : public LayerTreeNode {\npublic:\n  LayerTreeLayer() : LayerTreeNode(nullptr) {}\n  LayerTreeLayer(QgsLayerTreeLayer *layer) : LayerTreeNode(layer) {}\n\n  std::string layerId() const {\n    if (auto *layer = asLayer()) return layer->layerId().toStdString();\n    return \"\";\n  }\n\n  emscripten::val layer() const {\n    if (auto *treeLayer = asLayer()) return wrapLayer(treeLayer->layer());\n    return emscripten::val::null();\n  }\n\n  std::string renderLegend(float dpi) const {\n    auto *treeLayer = asLayer();\n    if (!treeLayer || !treeLayer->layer()) return \"\";\n    QgsLayerTree tempRoot;\n    tempRoot.addLayer(treeLayer->layer());\n    return renderLegendForTree(&tempRoot, dpi);\n  }\n\n  emscripten::val legendNodes() const {\n    emscripten::val result = emscripten::val::array();\n    auto *treeLayer = asLayer();\n    if (!treeLayer || !treeLayer->layer() || !treeLayer->layer()->legend()) return result;\n    qDeleteAll(legendNodesCache());\n    legendNodesCache() = treeLayer->layer()->legend()->createLayerTreeModelLegendNodes(treeLayer);\n    for (auto *node : legendNodesCache()) {\n      result.call<void>(\"push\", LegendNode(node));\n    }\n    return result;\n  }\n\nprivate:\n  QgsLayerTreeLayer *asLayer() const {\n    Q_ASSERT(_node && _node->nodeType() == QgsLayerTreeNode::NodeLayer);\n    return static_cast<QgsLayerTreeLayer *>(_node);\n  }\n};\n\ninline emscripten::val wrapNode(QgsLayerTreeNode *node) {\n  if (!node) return emscripten::val::null();\n  if (QgsLayerTree::isGroup(node))\n    return emscripten::val(LayerTreeGroup(static_cast<QgsLayerTreeGroup *>(node)));\n  return emscripten::val(LayerTreeLayer(static_cast<QgsLayerTreeLayer *>(node)));\n}\n\nEMSCRIPTEN_BINDINGS(QgsLayerTreeLayer) {\n  emscripten::class_<LayerTreeLayer, emscripten::base<LayerTreeNode>>(\"QgsLayerTreeLayer\")\n    .function(\"layerId\", &LayerTreeLayer::layerId)\n    .function(\"layer\", &LayerTreeLayer::layer)\n    .function(\"legendNodes\", &LayerTreeLayer::legendNodes)\n    .function(\"renderLegend\", &LayerTreeLayer::renderLegend);\n}\n"
  },
  {
    "path": "src/model/QgsLayerTreeLayer.ts",
    "content": "import type { QgsLayerTreeModelLegendNode } from \"./QgsLayerTreeModelLegendNode\";\nimport type { QgsLayerTreeNode } from \"./QgsLayerTreeNode\";\nimport type { QgsMapLayer } from \"./QgsMapLayer\";\n\nexport interface QgsLayerTreeLayer extends QgsLayerTreeNode {\n  layerId(): string;\n  layer(): QgsMapLayer | null;\n  legendNodes(): QgsLayerTreeModelLegendNode[];\n  renderLegend(dpi: number): string;\n}\n"
  },
  {
    "path": "src/model/QgsLayerTreeModelLegendNode.hpp",
    "content": "#pragma once\n\n#include <cmath>\n#include <string>\n\n#include <qgslayertreemodel.h>\n#include <qgslayertreemodellegendnode.h>\n#include <qgslegendrenderer.h>\n#include <qgslegendsettings.h>\n#include <qgsmaplayerlegend.h>\n#include <qgsrendercontext.h>\n#include <qgssymbol.h>\n\n#include <QBuffer>\n#include <QIcon>\n#include <QImage>\n#include <QPainter>\n#include <QPixmap>\n#include <QVariant>\n\n#include <emscripten/bind.h>\n#include <emscripten/val.h>\n\ninline QList<QgsLayerTreeModelLegendNode *> &legendNodesCache() {\n  static QList<QgsLayerTreeModelLegendNode *> cache;\n  return cache;\n}\n\ninline std::string imageToDataUrl(const QImage &image) {\n  if (image.isNull()) return \"\";\n  QByteArray ba;\n  QBuffer buffer(&ba);\n  buffer.open(QIODevice::WriteOnly);\n  image.save(&buffer, \"PNG\");\n  return \"data:image/png;base64,\" + ba.toBase64().toStdString();\n}\n\ninline std::string renderLegendForTree(QgsLayerTree *tree, float dpi) {\n  QgsLayerTreeModel model(tree);\n\n  QgsLegendSettings settings;\n  settings.setTitle(\"\");\n\n  QgsRenderContext context;\n  context.setScaleFactor(dpi / 25.4);\n\n  QgsLegendRenderer renderer(&model, settings);\n  QSizeF minSize = renderer.minimumSize(&context);\n\n  int width = static_cast<int>(std::ceil(minSize.width() * dpi / 25.4));\n  int height = static_cast<int>(std::ceil(minSize.height() * dpi / 25.4));\n  if (width <= 0 || height <= 0) return \"\";\n\n  QImage image(width, height, QImage::Format_ARGB32);\n  image.fill(Qt::transparent);\n  image.setDotsPerMeterX(static_cast<int>(dpi / 25.4 * 1000));\n  image.setDotsPerMeterY(static_cast<int>(dpi / 25.4 * 1000));\n\n  QPainter painter(&image);\n  painter.setRenderHint(QPainter::Antialiasing, true);\n  painter.scale(dpi / 25.4, dpi / 25.4);\n  QgsRenderContext renderContext = QgsRenderContext::fromQPainter(&painter);\n  renderContext.setScaleFactor(dpi / 25.4);\n\n  renderer.setLegendSize(minSize);\n  renderer.drawLegend(renderContext);\n  painter.end();\n\n  return imageToDataUrl(image);\n}\n\nclass LegendNode {\npublic:\n  LegendNode() : _node(nullptr) {}\n  LegendNode(QgsLayerTreeModelLegendNode *node) : _node(node) {}\n\n  bool isValid() const {\n    return _node != nullptr;\n  }\n\n  std::string label() const {\n    if (!_node) return \"\";\n    return _node->data(Qt::DisplayRole).toString().toStdString();\n  }\n\n  std::string symbolImage(int size) const {\n    if (!_node) return \"\";\n    if (auto *symNode = dynamic_cast<QgsSymbolLegendNode *>(_node)) {\n      if (const QgsSymbol *symbol = symNode->symbol()) {\n        QImage img = const_cast<QgsSymbol *>(symbol)->asImage(QSize(size, size));\n        return imageToDataUrl(img);\n      }\n    }\n    QVariant iconData = _node->data(Qt::DecorationRole);\n    if (iconData.canConvert<QIcon>()) {\n      QIcon icon = iconData.value<QIcon>();\n      QPixmap pix = icon.pixmap(QSize(size, size));\n      if (!pix.isNull()) return imageToDataUrl(pix.toImage());\n    }\n    return \"\";\n  }\n\nprivate:\n  QgsLayerTreeModelLegendNode *_node;\n};\n\nEMSCRIPTEN_BINDINGS(QgsLayerTreeModelLegendNode) {\n  emscripten::class_<LegendNode>(\"QgsLayerTreeModelLegendNode\")\n    .constructor<>()\n    .function(\"isValid\", &LegendNode::isValid)\n    .function(\"label\", &LegendNode::label)\n    .function(\"symbolImage\", &LegendNode::symbolImage);\n}\n"
  },
  {
    "path": "src/model/QgsLayerTreeModelLegendNode.ts",
    "content": "export interface QgsLayerTreeModelLegendNode {\n  isValid(): boolean;\n  label(): string;\n  symbolImage(size: number): string;\n}\n"
  },
  {
    "path": "src/model/QgsLayerTreeNode.hpp",
    "content": "#pragma once\n\n#include <string>\n\n#include <qgslayertree.h>\n\n#include <emscripten/bind.h>\n#include <emscripten/val.h>\n\nclass LayerTreeGroup;\nclass LayerTreeLayer;\n\nemscripten::val wrapNode(QgsLayerTreeNode *node);\n\nclass LayerTreeNode {\npublic:\n  LayerTreeNode() : _node(nullptr) {}\n  LayerTreeNode(QgsLayerTreeNode *node) : _node(node) {}\n  virtual ~LayerTreeNode() = default;\n\n  bool isValid() const {\n    return _node != nullptr;\n  }\n\n  int nodeType() const {\n    if (!_node) return -1;\n    return static_cast<int>(_node->nodeType());\n  }\n\n  bool isGroup() const {\n    return _node && _node->nodeType() == QgsLayerTreeNode::NodeGroup;\n  }\n\n  bool isLayer() const {\n    return _node && _node->nodeType() == QgsLayerTreeNode::NodeLayer;\n  }\n\n  std::string name() const {\n    if (!_node) return \"\";\n    return _node->name().toStdString();\n  }\n\n  void setName(std::string name) {\n    if (_node) _node->setName(QString::fromStdString(name));\n  }\n\n  bool isVisible() const {\n    if (!_node) return false;\n    return _node->isVisible();\n  }\n\n  bool itemVisibilityChecked() const {\n    if (!_node) return false;\n    return _node->itemVisibilityChecked();\n  }\n\n  void setItemVisibilityChecked(bool checked) {\n    if (_node) _node->setItemVisibilityChecked(checked);\n  }\n\n  bool isExpanded() const {\n    if (!_node) return false;\n    return _node->isExpanded();\n  }\n\n  void setExpanded(bool expanded) {\n    if (_node) _node->setExpanded(expanded);\n  }\n\n  emscripten::val children() const {\n    emscripten::val result = emscripten::val::array();\n    if (!_node) return result;\n    for (QgsLayerTreeNode *child : _node->children()) {\n      result.call<void>(\"push\", wrapNode(child));\n    }\n    return result;\n  }\n\n  QgsLayerTreeNode *nativeNode() const {\n    return _node;\n  }\n\nprotected:\n  QgsLayerTreeNode *_node;\n};\n\nEMSCRIPTEN_BINDINGS(QgsLayerTreeNode) {\n  emscripten::class_<LayerTreeNode>(\"QgsLayerTreeNode\")\n    .constructor<>()\n    .function(\"isValid\", &LayerTreeNode::isValid)\n    .function(\"nodeType\", &LayerTreeNode::nodeType)\n    .function(\"isGroup\", &LayerTreeNode::isGroup)\n    .function(\"isLayer\", &LayerTreeNode::isLayer)\n    .property(\"name\", &LayerTreeNode::name, &LayerTreeNode::setName)\n    .function(\"isVisible\", &LayerTreeNode::isVisible)\n    .property(\n      \"itemVisibilityChecked\",\n      &LayerTreeNode::itemVisibilityChecked,\n      &LayerTreeNode::setItemVisibilityChecked)\n    .property(\"expanded\", &LayerTreeNode::isExpanded, &LayerTreeNode::setExpanded)\n    .function(\"children\", &LayerTreeNode::children);\n}\n"
  },
  {
    "path": "src/model/QgsLayerTreeNode.ts",
    "content": "import type { QgsLayerTreeGroup } from \"./QgsLayerTreeGroup\";\nimport type { QgsLayerTreeLayer } from \"./QgsLayerTreeLayer\";\n\nexport enum NodeType {\n  NodeGroup = 0,\n  NodeLayer = 1,\n}\n\nexport interface QgsLayerTreeNode {\n  name: string;\n  itemVisibilityChecked: boolean;\n  expanded: boolean;\n\n  isValid(): boolean;\n  nodeType(): NodeType;\n  isGroup(): boolean;\n  isLayer(): boolean;\n  isVisible(): boolean;\n  children(): (QgsLayerTreeGroup | QgsLayerTreeLayer)[];\n}\n"
  },
  {
    "path": "src/model/QgsMapLayer.hpp",
    "content": "#pragma once\n\n#include <string>\n\n#include <qgsmaplayer.h>\n#include <qgsvectorlayer.h>\n\n#include <emscripten/bind.h>\n#include <emscripten/val.h>\n\nclass VectorLayer;\n\nemscripten::val wrapLayer(QgsMapLayer *layer);\n\nclass MapLayer {\npublic:\n  MapLayer() : _layer(nullptr) {}\n  MapLayer(QgsMapLayer *layer) : _layer(layer) {}\n  virtual ~MapLayer() = default;\n\n  bool isValid() const {\n    return _layer != nullptr;\n  }\n\n  int type() const {\n    if (!_layer) return -1;\n    return static_cast<int>(_layer->type());\n  }\n\n  std::string id() const {\n    if (!_layer) return \"\";\n    return _layer->id().toStdString();\n  }\n\n  std::string name() const {\n    if (!_layer) return \"\";\n    return _layer->name().toStdString();\n  }\n\n  void setName(std::string name) {\n    if (_layer) _layer->setName(QString::fromStdString(name));\n  }\n\n  double opacity() const {\n    if (!_layer) return 1.0;\n    return _layer->opacity();\n  }\n\n  void setOpacity(double opacity) {\n    if (_layer) _layer->setOpacity(opacity);\n  }\n\nprotected:\n  QgsMapLayer *_layer;\n};\n\nclass VectorLayer : public MapLayer {\npublic:\n  VectorLayer() : MapLayer(nullptr) {}\n  VectorLayer(QgsVectorLayer *layer) : MapLayer(layer) {}\n\n  std::string subsetString() const {\n    if (auto *vl = asVectorLayer()) return vl->subsetString().toStdString();\n    return \"\";\n  }\n\n  bool setSubsetString(std::string subset) {\n    if (auto *vl = asVectorLayer()) return vl->setSubsetString(QString::fromStdString(subset));\n    return false;\n  }\n\nprivate:\n  QgsVectorLayer *asVectorLayer() const {\n    Q_ASSERT(_layer && _layer->type() == Qgis::LayerType::Vector);\n    return static_cast<QgsVectorLayer *>(_layer);\n  }\n};\n\ninline emscripten::val wrapLayer(QgsMapLayer *layer) {\n  if (!layer) return emscripten::val::null();\n  if (layer->type() == Qgis::LayerType::Vector)\n    return emscripten::val(VectorLayer(static_cast<QgsVectorLayer *>(layer)));\n  return emscripten::val(MapLayer(layer));\n}\n\nEMSCRIPTEN_BINDINGS(QgsMapLayer) {\n  emscripten::class_<MapLayer>(\"QgsMapLayer\")\n    .constructor<>()\n    .function(\"isValid\", &MapLayer::isValid)\n    .function(\"type\", &MapLayer::type)\n    .function(\"id\", &MapLayer::id)\n    .property(\"name\", &MapLayer::name, &MapLayer::setName)\n    .property(\"opacity\", &MapLayer::opacity, &MapLayer::setOpacity);\n\n  emscripten::class_<VectorLayer, emscripten::base<MapLayer>>(\"QgsVectorLayer\")\n    .function(\"subsetString\", &VectorLayer::subsetString)\n    .function(\"setSubsetString\", &VectorLayer::setSubsetString);\n}\n"
  },
  {
    "path": "src/model/QgsMapLayer.ts",
    "content": "export enum LayerType {\n  Vector = 0,\n  Raster = 1,\n  Plugin = 2,\n  Mesh = 3,\n  VectorTile = 4,\n  Annotation = 5,\n  PointCloud = 6,\n  Group = 7,\n  TiledScene = 8,\n}\n\nexport interface QgsMapLayer {\n  name: string;\n  opacity: number;\n\n  isValid(): boolean;\n  type(): LayerType;\n  id(): string;\n}\n\nexport interface QgsVectorLayer extends QgsMapLayer {\n  subsetString(): string;\n  setSubsetString(subset: string): boolean;\n}\n"
  },
  {
    "path": "src/model/QgsMapRendererJob.hpp",
    "content": "#pragma once\n\n#include <qgsmaprendererjob.h>\n\n#include <emscripten/bind.h>\n\nstatic void QgsMapRendererJob_finished(QgsMapRendererJob &job, emscripten::val callback) {\n  QObject::connect(&job, &QgsMapRendererJob::finished, [callback] { callback(); });\n}\n\nEMSCRIPTEN_BINDINGS(QgsMapRendererJob) {\n  emscripten::class_<QgsMapRendererJob>(\"QgsMapRendererJob\")\n    .function(\"cancel\", &QgsMapRendererJob::cancel)\n    .function(\"cancelWithoutBlocking\", &QgsMapRendererJob::cancelWithoutBlocking)\n    .function(\"isActive\", &QgsMapRendererJob::isActive)\n    .function(\"renderingTime\", &QgsMapRendererJob::renderingTime)\n    // signals\n    .function(\"finished\", &QgsMapRendererJob_finished);\n}\n"
  },
  {
    "path": "src/model/QgsMapRendererJob.ts",
    "content": "/**\n * Abstract base class for map rendering implementations\n *\n * {@link https://api.qgis.org/api/classQgsMapRendererJob.html}\n */\nexport interface QgsMapRendererJob {\n  /**\n   * Stop the rendering job - does not return until the job has terminated\n   */\n  cancel(): void;\n\n  /**\n   * Triggers cancellation of the rendering job without blocking.\n   */\n  cancelWithoutBlocking(): void;\n\n  /**\n   * Tell whether the rendering job is currently running in background.\n   * @returns True if the job is active, false otherwise.\n   */\n  isActive(): boolean;\n\n  /**\n   * Returns the total time it took to finish the job (in milliseconds)\n   * @returns The rendering time in milliseconds.\n   */\n  renderingTime(): number;\n\n  /**\n   * Registers a callback function to be called when the map rendering job is finished.\n   * @param callback The callback function to be called.\n   */\n  finished(callback: () => void): void;\n}\n"
  },
  {
    "path": "src/model/QgsMapRendererParallelJob.hpp",
    "content": "#pragma once\n\n#include <qgsmaprendererparalleljob.h>\n\n#include <emscripten/bind.h>\n\n#include \"./QgsMapRendererQImageJob.hpp\"\n\nEMSCRIPTEN_BINDINGS(QgsMapRendererParallelJob) {\n  emscripten::class_<QgsMapRendererParallelJob, emscripten::base<QgsMapRendererQImageJob>>(\n    \"QgsMapRendererParallelJob\")\n    .function(\"cancel\", &QgsMapRendererParallelJob::cancel)\n    .function(\"cancelWithoutBlocking\", &QgsMapRendererParallelJob::cancelWithoutBlocking)\n    .function(\"isActive\", &QgsMapRendererParallelJob::isActive)\n    .function(\"renderingTime\", &QgsMapRendererParallelJob::renderingTime);\n}\n"
  },
  {
    "path": "src/model/QgsMapRendererParallelJob.ts",
    "content": "import { QgsMapRendererQImageJob } from \"./QgsMapRendererQImageJob\";\n\n/**\n * Job implementation that renders all layers in parallel\n *\n * {@link https://api.qgis.org/api/classQgsMapRendererParallelJob.html}\n */\nexport interface QgsMapRendererParallelJob extends QgsMapRendererQImageJob {}\n"
  },
  {
    "path": "src/model/QgsMapRendererQImageJob.hpp",
    "content": "#pragma once\n\n#include <qgsmaprendererjob.h>\n\n#include <emscripten/bind.h>\n\n#include \"./QgsMapRendererJob.hpp\"\n\nstatic emscripten::val QgsMapRendererQImageJob_renderedImage(QgsMapRendererQImageJob &job) {\n  auto image = std::move(job.renderedImage()).convertToFormat(QImage::Format_RGBA8888);\n  return emscripten::val(emscripten::typed_memory_view(\n    image.width() * image.height() * 4, (const unsigned char *)image.constBits()));\n}\n\nEMSCRIPTEN_BINDINGS(QgsMapRendererQImageJob) {\n  emscripten::class_<QgsMapRendererQImageJob, emscripten::base<QgsMapRendererJob>>(\n    \"QgsMapRendererQImageJob\")\n    .function(\"renderedImage\", &QgsMapRendererQImageJob_renderedImage);\n}\n"
  },
  {
    "path": "src/model/QgsMapRendererQImageJob.ts",
    "content": "import { QgsMapRendererJob } from \"./QgsMapRendererJob\";\n\n/**\n * Intermediate base class adding functionality that allows client to query the rendered image\n *\n * {@link https://api.qgis.org/api/classQgsMapRendererQImageJob.html}\n */\nexport interface QgsMapRendererQImageJob extends QgsMapRendererJob {\n  /**\n   * Returns the (preview or final) rendered image as a byte array\n   * @returns The rendered image as an emscripten typed_memory_view.\n   */\n  renderedImage(): ArrayBufferLike;\n}\n"
  },
  {
    "path": "src/model/QgsPointXY.hpp",
    "content": "\n#include <qgspointxy.h>\n\n#include <emscripten/bind.h>\n\nEMSCRIPTEN_BINDINGS(QgsPointXY) {\n  emscripten::class_<QgsPointXY>(\"QgsPointXY\")\n    .constructor<>()\n    .property(\"x\", &QgsPointXY::x, &QgsPointXY::setX)\n    .property(\"y\", &QgsPointXY::y, &QgsPointXY::setY);\n}\n"
  },
  {
    "path": "src/model/QgsPointXY.ts",
    "content": "/**\n * A class to represent a 2D point.\n *\n * {@link https://api.qgis.org/api/classQgsPointXY.html}\n */\nexport interface QgsPointXY {\n  x: number;\n  y: number;\n}\n\n/**\n * The {@link QgsPointXY} constructors.\n */\nexport interface QgsPointXYConstructors {\n  QgsPointXY: { new (): QgsPointXY };\n}\n"
  },
  {
    "path": "src/model/QgsRectangle.hpp",
    "content": "\n#include <qgsrectangle.h>\n\n#include <emscripten/bind.h>\n\nstatic void QgsRectangle_scale(QgsRectangle &r, double f) {\n  r.scale(f);\n}\n\nstatic void QgsRectangle_move(QgsRectangle &r, double dx, double dy) {\n  r += QgsVector(dx, dy);\n}\n\nEMSCRIPTEN_BINDINGS(QgsRectangle) {\n  emscripten::class_<QgsRectangle>(\"QgsRectangle\")\n    .constructor<>()\n    .constructor<double, double, double, double>()\n    .property(\"xMinimum\", &QgsRectangle::xMinimum, &QgsRectangle::setXMinimum)\n    .property(\"yMinimum\", &QgsRectangle::yMinimum, &QgsRectangle::setYMinimum)\n    .property(\"xMaximum\", &QgsRectangle::xMaximum, &QgsRectangle::setXMaximum)\n    .property(\"yMaximum\", &QgsRectangle::yMaximum, &QgsRectangle::setYMaximum)\n    .function(\"scale\", &QgsRectangle_scale)\n    .function(\"move\", &QgsRectangle_move)\n    .function(\"center\", &QgsRectangle::center);\n}\n"
  },
  {
    "path": "src/model/QgsRectangle.ts",
    "content": "import { QgsPointXY } from \"./QgsPointXY\";\n\n/**\n * A rectangle specified with double values.\n *\n * {@link https://api.qgis.org/api/classQgsRectangle.html}\n */\nexport interface QgsRectangle {\n  xMaximum: number;\n  xMinimum: number;\n  yMaximum: number;\n  yMinimum: number;\n\n  scale(factor: number): void;\n  move(dx: number, dy: number): void;\n  center(): QgsPointXY;\n}\n\n/**\n * The {@link QgsRectangle} constructors.\n */\nexport interface QgsRectangleConstructors {\n  QgsRectangle: {\n    new (): QgsRectangle;\n    new (xMin: number, yMin: number, xMax: number, yMax: number): QgsRectangle;\n  };\n}\n"
  },
  {
    "path": "src/qgis-js.cpp",
    "content": "\n#include \"cpl_conv.h\"\n#include <expat.h>\n#include <gdal.h>\n#include <geos_c.h>\n#include <proj.h>\n#include <qt6keychain/keychain.h>\n#include <spatialindex/capi/sidx_api.h>\n#include <sqlite3.h>\n#include <zip.h>\n\n#include <qgis.h>\n#include <qgsapplication.h>\n#include <qgscoordinatereferencesystem.h>\n#include <qgslayertree.h>\n#include <qgsmaprenderercustompainterjob.h>\n#include <qgsmaprenderersequentialjob.h>\n#include <qgsmapsettings.h>\n#include <qgsproject.h>\n#include <qgsprojutils.h>\n#include <qgsproviderregistry.h>\n#include <qgssettingsregistrycore.h>\n\n#include <QFileInfo>\n#include <QGuiApplication>\n#include <QTemporaryDir>\n#include <QTimer>\n#include <QtGlobal>\n\n#include <emscripten/val.h>\n\nstatic const QTemporaryDir temp;\nstatic QGuiApplication *app;\n\nstatic const bool testLibraries = false;\n\nint main(int argc, char *argv[]) {\n  // set PROJ search paths\n  const char *path = \"/proj\";\n  proj_context_set_search_paths(nullptr, 1, &path);\n\n  // START optimizations\n  sqlite3_vfs_register(sqlite3_vfs_find(\"unix-none\"), 1);\n\n  CPLSetConfigOption(\"DO_NOT_ENABLE_WAL\", \"YES\");\n  CPLSetConfigOption(\"OGR_SQLITE_JOURNAL\", \"OFF\");\n  CPLSetConfigOption(\"OGR_SQLITE_CACHE\", \"128\");\n  CPLSetConfigOption(\"OGR_SQLITE_SYNCHRONOUS\", \"OFF\");\n  CPLSetConfigOption(\"NOLOCK\", \"YES\");\n  CPLSetConfigOption(\"IMMUTABLE\", \"YES\");\n  // END optimizations\n\n  // needed?\n  QCoreApplication::setOrganizationName(\"QGIS\");\n  QCoreApplication::setOrganizationDomain(\"qgis.org\");\n  QCoreApplication::setApplicationName(\"qgis-js\");\n\n  // prevent warning from QWasmLocalStorageSettingsPrivate\n  QSettings::setDefaultFormat(QSettings::IniFormat);\n  QSettings::setPath(\n    QSettings::IniFormat, QSettings::UserScope, temp.path() + QString(\"/settings\"));\n\n  app = new QGuiApplication(argc, argv);\n  // qDebug() << \"QgsApplication::init\";\n  QgsApplication::init(temp.path());\n  QgsApplication::setPkgDataPath(\"/qgis\"); // as set in CMakeLists.txt\n\n  int maxThreads = 4;\n  emscripten::val qgisJsMaxThreads = emscripten::val::module_property(\"qgisJsMaxThreads\");\n  if (!qgisJsMaxThreads.isUndefined()) {\n    int maxThreadsValue = qgisJsMaxThreads.as<int>();\n    if (maxThreadsValue > 0) {\n      maxThreads = maxThreadsValue;\n    }\n  }\n  QgsApplication::setMaxThreads(maxThreads);\n\n  QgsSettingsRegistryCore::settingsLayerParallelLoading->setValue(false);\n\n  if (testLibraries) {\n\n    // version information\n    qDebug() << \"qgis \" << Qgis::version() << \" (\" << Qgis::devVersion() << \")\";\n    qDebug() << \"qt \" << QString(QT_VERSION_STR);\n    qDebug() << \"gdal \" << QString(GDAL_RELEASE_NAME);\n    PJ_INFO info = proj_info();\n    const QString projVersionCompiled{QStringLiteral(\"%1.%2.%3\")\n                                        .arg(PROJ_VERSION_MAJOR)\n                                        .arg(PROJ_VERSION_MINOR)\n                                        .arg(PROJ_VERSION_PATCH)};\n    qDebug() << \"proj \" << projVersionCompiled;\n    qDebug() << \"geos \" << QString(GEOS_CAPI_VERSION);\n    qDebug() << \"sqlite \" << QString{SQLITE_VERSION};\n    // no PDAL\n    // no postgres\n    // no spatialite\n    qDebug() << \"OS \" << QSysInfo::prettyProductName(); // says emscripten + version\n#ifdef QGISDEBUG\n    qDebug() << \"debug build of qgis\";\n#endif\n\n    qDebug() << \"qgis pkg data path: \" << QgsApplication::pkgDataPath();\n    qDebug() << \"srs path\" << QgsApplication::srsDatabaseFilePath();\n    qDebug() << \"proj search paths:\" << QgsProjUtils::searchPaths();\n\n    // qgis providers\n    QStringList providerList = QgsProviderRegistry::instance()->providerList();\n    qDebug() << \"providers\" << providerList;\n\n    // gdal drivers\n    int driverCount = GDALGetDriverCount();\n    QStringList gdalDrivers;\n    // qDebug() << \"gdal drivers\" << driverCount;\n    for (int i = 0; i < driverCount; ++i) {\n      GDALDriverH dr = GDALGetDriver(i);\n      QString driverName = GDALGetDescription(dr);\n      gdalDrivers << driverName;\n      // QString longName = GDALGetMetadataItem( dr, \"DMD_LONGNAME\", \"\" );\n      // bool isRaster = QString( GDALGetMetadataItem( dr, GDAL_DCAP_RASTER,\n      // nullptr ) ) == QLatin1String( \"YES\" ); bool isVector =  QString(\n      // GDALGetMetadataItem( dr, GDAL_DCAP_VECTOR, nullptr ) ) ==\n      // QLatin1String( \"YES\" ); qDebug() << driverName << isRaster << isVector\n      // << longName;  // quite verbose with 100+ drivers!\n    }\n    qDebug() << \"gdal drivers \" << gdalDrivers;\n\n    if (!QFileInfo::exists(QgsApplication::srsDatabaseFilePath())) {\n      qDebug() << \"srs db does not exist!\" << QgsApplication::srsDatabaseFilePath();\n      return 1;\n    }\n\n    if (!QFileInfo::exists(\"/proj/proj.db\")) {\n      qDebug() << \"proj db does not exist!\"\n               << \"/proj/proj.db\";\n      return 1;\n    }\n\n    if (QgsCoordinateReferenceSystem(\"EPSG:27700\").toWkt().isEmpty()) {\n      qDebug() << \"something wrong with CRS database!\";\n      return 1;\n    }\n  }\n\n  return 0;\n}\n"
  },
  {
    "path": "src/qt.conf",
    "content": "[Paths]\nPrefix=/\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  }\n}\n"
  },
  {
    "path": "vcpkg.json",
    "content": "{\n  \"name\": \"qgis-js\",\n  \"version\": \"0.1.0\",\n  \"vcpkg-configuration\": {\n    \"overlay-triplets\": [\"./build/vcpkg-triplets\"],\n    \"overlay-ports\": [\"./build/vcpkg-ports\"]\n  },\n  \"dependencies\": [\n    \"geos\",\n    {\n      \"name\": \"sqlite3\",\n      \"default-features\": false,\n      \"features\": [\"rtree\", \"json1\", \"fts3\"]\n    },\n    {\n      \"name\": \"proj\",\n      \"default-features\": false,\n      \"features\": [\"tiff\"]\n    },\n    {\n      \"name\": \"gdal\",\n      \"default-features\": false,\n      \"features\": [\"geos\", \"sqlite3\"]\n    },\n    \"libspatialindex\",\n    \"expat\",\n    {\n      \"name\": \"libzip\",\n      \"default-features\": false\n    },\n    \"protobuf\",\n    {\n      \"name\": \"freetype\",\n      \"default-features\": false,\n      \"features\": [\"zlib\"]\n    },\n    {\n      \"name\": \"freetype\",\n      \"default-features\": false,\n      \"host\": true\n    },\n    {\n      \"name\": \"qtbase\",\n      \"default-features\": false,\n      \"features\": [\n        \"opengl\",\n        \"gles3\",\n        \"concurrent\",\n        \"freetype\",\n        \"gui\",\n        \"jpeg\",\n        \"network\",\n        \"widgets\",\n        \"sql\",\n        \"sql-sqlite\",\n        \"testlib\"\n      ]\n    },\n    {\n      \"name\": \"qtbase\",\n      \"host\": true,\n      \"default-features\": false\n    },\n    {\n      \"name\": \"qt5compat\",\n      \"default-features\": false,\n      \"features\": [\"textcodec\"]\n    },\n    {\n      \"name\": \"qtkeychain-qt6\",\n      \"default-features\": false\n    },\n    {\n      \"name\": \"qtsvg\",\n      \"default-features\": false\n    },\n    {\n      \"name\": \"qtmultimedia\",\n      \"default-features\": false\n    },\n    {\n      \"name\": \"qgis\",\n      \"default-features\": false\n    }\n  ]\n}\n"
  }
]