Repository: qgis/qgis-js Branch: main Commit: 8feda36392eb Files: 133 Total size: 352.6 KB Directory structure: gitextract_sch8wsm5/ ├── .clang-format ├── .editorconfig ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── .gitmodules ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── .vscode/ │ ├── c_cpp_properties.json │ └── settings.json ├── CHANGELOG.md ├── CMakeLists.txt ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build/ │ ├── actions/ │ │ ├── clean.ts │ │ ├── compile.ts │ │ ├── install.ts │ │ ├── lib/ │ │ │ ├── BuildType.ts │ │ │ └── QgisJsOptions.ts │ │ ├── libs.ts │ │ └── size.ts │ ├── scripts/ │ │ ├── clean.sh │ │ ├── compile.sh │ │ └── install.sh │ ├── vcpkg-ports/ │ │ ├── libspatialindex/ │ │ │ ├── portfile.cmake │ │ │ └── vcpkg.json │ │ └── qgis/ │ │ ├── portfile.cmake │ │ └── vcpkg.json │ ├── vcpkg-toolchains/ │ │ └── qgis-js.cmake │ ├── vcpkg-triplets/ │ │ └── wasm32-emscripten-qt-threads.cmake │ └── vite/ │ ├── CrossOriginIsolationPlugin.ts │ ├── DirectoryListingPlugin.ts │ └── QgisRuntimePlugin.ts ├── docs/ │ ├── architecture.md │ ├── bundling.md │ ├── ci.md │ ├── compatibility.md │ ├── debugging.md │ ├── examples/ │ │ ├── qgis-js-example-api/ │ │ │ ├── index.html │ │ │ ├── main.js │ │ │ ├── package.json │ │ │ └── vite.config.js │ │ └── qgis-js-example-ol/ │ │ ├── index.html │ │ ├── main.js │ │ ├── package.json │ │ ├── style.css │ │ └── vite.config.js │ ├── performance.md │ └── profiling.md ├── package.json ├── packages/ │ ├── qgis-js/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── QgisApiAdapter.ts │ │ │ ├── emscripten.ts │ │ │ ├── index.ts │ │ │ ├── loader.ts │ │ │ └── runtime.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ ├── qgis-js-ol/ │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ ├── QgisCanvasDataSource.ts │ │ │ ├── QgisJobDataSource.ts │ │ │ ├── QgisXYZDataSource.ts │ │ │ └── index.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── qgis-js-utils/ │ ├── README.md │ ├── package.json │ ├── src/ │ │ ├── fs/ │ │ │ ├── FileSystem.ts │ │ │ ├── GithubProject.ts │ │ │ ├── LocalProject.ts │ │ │ ├── Project.ts │ │ │ ├── RemoteProject.ts │ │ │ └── index.ts │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-workspace.yaml ├── qgis-js.ts ├── sites/ │ ├── dev/ │ │ ├── index.html │ │ ├── package.json │ │ ├── public/ │ │ │ └── projects/ │ │ │ └── village/ │ │ │ ├── buildings.dbf │ │ │ ├── buildings.prj │ │ │ ├── buildings.qpj │ │ │ ├── buildings.shp │ │ │ ├── buildings.shx │ │ │ ├── project.qgs │ │ │ └── rgb.tif │ │ ├── src/ │ │ │ ├── demo.css │ │ │ ├── index.ts │ │ │ ├── js.ts │ │ │ ├── layers.ts │ │ │ └── ol.ts │ │ ├── tsconfig.json │ │ └── vite.config.ts │ └── performance/ │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── playwright.config.ts │ ├── report.html │ ├── src/ │ │ └── index.ts │ ├── tests/ │ │ └── performance.spec.ts │ ├── tsconfig.json │ └── vite.config.ts ├── src/ │ ├── api/ │ │ ├── QgisApi.cpp │ │ ├── QgisApi.ts │ │ └── QgisModel.ts │ ├── model/ │ │ ├── QgsLayerTreeGroup.hpp │ │ ├── QgsLayerTreeGroup.ts │ │ ├── QgsLayerTreeLayer.hpp │ │ ├── QgsLayerTreeLayer.ts │ │ ├── QgsLayerTreeModelLegendNode.hpp │ │ ├── QgsLayerTreeModelLegendNode.ts │ │ ├── QgsLayerTreeNode.hpp │ │ ├── QgsLayerTreeNode.ts │ │ ├── QgsMapLayer.hpp │ │ ├── QgsMapLayer.ts │ │ ├── QgsMapRendererJob.hpp │ │ ├── QgsMapRendererJob.ts │ │ ├── QgsMapRendererParallelJob.hpp │ │ ├── QgsMapRendererParallelJob.ts │ │ ├── QgsMapRendererQImageJob.hpp │ │ ├── QgsMapRendererQImageJob.ts │ │ ├── QgsPointXY.hpp │ │ ├── QgsPointXY.ts │ │ ├── QgsRectangle.hpp │ │ └── QgsRectangle.ts │ ├── qgis-js.cpp │ └── qt.conf ├── tsconfig.json └── vcpkg.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ #see https://clang.llvm.org/docs/ClangFormatStyleOptions.html Language: Cpp ColumnLimit: 100 ContinuationIndentWidth: 2 UseTab: Never IndentWidth: 2 TabWidth: 2 IndentCaseLabels: true IncludeBlocks: Preserve SortIncludes: true SortUsingDeclarations: false AlignConsecutiveMacros: false AlignEscapedNewlines: DontAlign AlignAfterOpenBracket: AlwaysBreak AlignOperands: false AlignTrailingComments: false BinPackArguments: false BinPackParameters: false SpacesInContainerLiterals: false Cpp11BracedListStyle: true AllowShortFunctionsOnASingleLine: Empty AllowShortIfStatementsOnASingleLine: Always FixNamespaceComments: false ReflowComments: false NamespaceIndentation: All BreakStringLiterals: false ConstructorInitializerIndentWidth: 2 SpaceBeforeCtorInitializerColon: true BreakBeforeInheritanceComma: false BreakInheritanceList: AfterColon BreakConstructorInitializersBeforeComma: false BreakConstructorInitializers: AfterColon ConstructorInitializerAllOnOneLineOrOnePerLine: true ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: workflow_dispatch # push: # branches: # - main # pull_request: # branches: # - main permissions: contents: read pages: write id-token: write concurrency: group: "pages" cancel-in-progress: true jobs: build-qgis-js: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - name: Set Swap Space if: ${{ !env.ACT }} uses: pierotofy/set-swap-space@master with: swap-size-gb: 8 - name: Update apt cache run: >- sudo apt-get update - name: Install system dependencies run: >- sudo apt-get install -y ninja-build pkg-config flex bison autoconf autoconf-archive automake libtool - name: Free Disk Space (Ubuntu) if: ${{ !env.ACT }} uses: jlumbroso/free-disk-space@main with: tool-cache: false swap-storage: false - name: Setup node from package.json uses: actions/setup-node@v4 with: node-version-file: "package.json" - uses: pnpm/action-setup@v2 - name: Install dependencies run: pnpm install - name: Compile qgis-js run: pnpm run compile:release - name: Save logs on failure if: ${{ failure() }} uses: actions/upload-artifact@v4 with: name: vcpkg-buildtrees-logs path: build/vcpkg/buildtrees/**/*.log - name: Build qgis-js packages run: pnpm run build - uses: actions/upload-artifact@v4 with: name: "package-qgis-js" path: | packages/qgis-js/package.json packages/qgis-js/README.md packages/qgis-js/dist/**/* - uses: actions/upload-artifact@v4 with: name: "package-qgis-js-utils" path: | packages/qgis-js-utils/package.json packages/qgis-js-utils/README.md packages/qgis-js-utils/dist/**/* - uses: actions/upload-artifact@v4 with: name: "package-qgis-js-ol" path: | packages/qgis-js-ol/package.json packages/qgis-js-ol/README.md packages/qgis-js-ol/dist/**/* - name: Build site run: pnpm run dev:build - name: Setup Pages uses: actions/configure-pages@v4 - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: "./sites/dev/dist" - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .gitignore ================================================ package-lock.json CMakeLists.txt.user build/wasm node_modules dist packages/*/dist packages/*/etc bin/act docs/api .DS_Store ================================================ FILE: .gitmodules ================================================ [submodule "build/vcpkg"] path = build/vcpkg url = https://github.com/microsoft/vcpkg.git [submodule "build/emsdk"] path = build/emsdk url = https://github.com/emscripten-core/emsdk.git ================================================ FILE: .nvmrc ================================================ v22.16.0 ================================================ FILE: .prettierignore ================================================ public dist node_modules build/wasm build/emsdk build/vcpkg build/vcpkg-ports build/vcpkg-toolchains build/vcpkg-triplets build/qt-patches pnpm-lock.yaml ================================================ FILE: .prettierrc.json ================================================ {} ================================================ FILE: .vscode/c_cpp_properties.json ================================================ { "configurations": [ { "name": "Linux", "includePath": [ "${workspaceFolder}/build/wasm/vcpkg_installed/wasm32-emscripten-qt-threads/include", "${workspaceFolder}/build/wasm/vcpkg_installed/wasm32-emscripten-qt-threads/include/qgis", "${workspaceFolder}/build/emsdk/upstream/emscripten/system/include" ], "defines": [], "compilerPath": "/usr/bin/gcc", "cStandard": "c11", "cppStandard": "c++17", "intelliSenseMode": "clang-x64" } ], "version": 4 } ================================================ FILE: .vscode/settings.json ================================================ { "workbench.editor.enablePreview": false, "editor.tabSize": 2, "editor.formatOnSave": true, "files.insertFinalNewline": true, "files.associations": { "**/package.json": "json", "**/*.json": "jsonc", "**/*.svg": "xml", "**/*.cpp": "cpp", "**/*.hpp": "cpp", "functional": "cpp" }, "terminal.integrated.env.linux": { "PATH": "${workspaceRoot}/build/emsdk:${workspaceRoot}/build/emsdk/upstream/emscripten:${workspaceRoot}/build/emsdk/upstream/bin:${workspaceRoot}/build/vcpkg:${env:PATH}" }, "search.exclude": { "build/emsdk": true, "build/vcpkg": true, "docs/api": true, "*/*/node_modules": true, "*/*/dist": true, "*/*/temp": true, "*/*/.vitepress/dist": true }, "typescript.tsdk": "node_modules/typescript/lib", "C_Cpp.autoAddFileAssociations": false, "C_Cpp.clang_format_path": "${workspaceRoot}/node_modules/clang-format/bin/linux_x64/clang-format", "cmake.cmakePath": "${workspaceFolder}/build/vcpkg/downloads/tools/cmake-3.27.1-linux/cmake-3.27.1-linux-x86_64/bin/cmake", "cmake.environment": { "VCPKG_BINARY_SOURCES": "clear" }, "cmake.configureSettings": { "CMAKE_TOOLCHAIN_FILE": "${workspaceFolder}/build/vcpkg/scripts/buildsystems/vcpkg.cmake", "VCPKG_CHAINLOAD_TOOLCHAIN_FILE": "${workspaceFolder}/build/vcpkg-toolchains/qgis-js.cmake", "VCPKG_TARGET_TRIPLET": "wasm32-emscripten-qt-threads", "VCPKG_OVERLAY_TRIPLETS": "${workspaceFolder}/build/vcpkg-triplets", "VCPKG_OVERLAY_PORTS": "${workspaceFolder}/build/vcpkg-ports" }, "cmake.buildDirectory": "${workspaceFolder}/build/wasm", "cmake.emscriptenSearchDirs": ["build/emsdk/upstream/emscripten"], "prettier.configPath": ".prettierrc.json", "prettier.documentSelectors": [ "**/*.{js,cjs,json,ts,vue,html,css,svg,yaml,md,webmanifest,clang-format,npmrc}" ], "clang-format.executable": "${workspaceRoot}/node_modules/clang-format/bin/linux_x64/clang-format" } ================================================ FILE: CHANGELOG.md ================================================ This document describes changes between tagged qgis-js versions ## 4.1.0 (in development) - Replaced `mapLayers()` with `layerTreeRoot()`. (#25) - Exposing the full layer tree hierarchy (groups, nested layers, visibility, expand/collapse). - Use `layerTreeRoot().findLayers()` as a migration path for flat layer access. - Added `QgsMapLayer` and `QgsVectorLayer` wrappers with `name`, `opacity`, `id()`, `type()`, and `subsetString()`. (#59) - Added layer legend support. (#59) - `QgsLayerTreeLayer.legendNodes()` returns individual legend entries with `label()` and `symbolImage()`. - `renderLegend()` renders the full project legend as a high-DPI PNG (base64 data URL). - `QgsLayerTreeGroup.renderLegend()` and `QgsLayerTreeLayer.renderLegend()` for subtree/single-layer legends. - Added optional `layerIds` parameter to all render functions (`renderImage`, `renderXYZTile`, `renderJob`, `fullExtent`, `renderLegend`). (#59) - Enables rendering subsets of layers and composing multiple OL layers from different QGIS layer groups. - All three OL data sources (`QgisCanvasDataSource`, `QgisXYZDataSource`, `QgisJobDataSource`) accept `layerIds` in options. - Added `loadLayerDefinition()` to load .qlr files at runtime into the project layer tree. (#59) ## 4.0.0 (16. March 2026) - Major version bump to align with QGIS 4 (based on QGIS 4.0.0). (#39) - Upstreamed patches to the QGIS repository for long-term maintainability. - Made possible by the QGIS.org grant programme 2025. - Special thanks to Matthias for the substantial contributions. - Dependency updates: - Updated Qt to 6.10.1 - Updated emsdk to 5.0.2 - Updated Node to 22.16.0 - Updated Vite to 8.0.0 - Updated OpenLayers ("ol") to 10.8.0 - Refactored `Rectangle` and `PointXY` types to `QgsRectangle` and `QgsPointXY` across the codebase. This is a breaking API change. (#53) - Added API to get/set global and project variables. - Switched from pinned vcpkg to git submodule (based on QGIS). ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.15) project(qgis-js CXX) set(CMAKE_CXX_STANDARD 17) find_package(unofficial-sqlite3 CONFIG REQUIRED) find_package(PROJ CONFIG REQUIRED) find_package(GEOS CONFIG REQUIRED) find_package(GDAL CONFIG REQUIRED) find_package(expat CONFIG REQUIRED) find_package(libzip CONFIG REQUIRED) find_package(exiv2 CONFIG REQUIRED) find_package(Protobuf CONFIG REQUIRED) find_package(zstd CONFIG REQUIRED) find_package(Qt6 REQUIRED COMPONENTS Core Gui Xml Network Concurrent Core5Compat PrintSupport Widgets) find_package(Qt6Keychain CONFIG REQUIRED) # Set debug prefix early so all find_library calls use it if (CMAKE_BUILD_TYPE STREQUAL "Debug") set(qgis_debug_prefix "debug/") set(spatialindex_name "spatialindexd") else() set(qgis_debug_prefix "") set(spatialindex_name "spatialindex") endif() find_library(SPATIALINDEX_LIBRARY ${spatialindex_name} PATHS ${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/${qgis_debug_prefix}lib NO_DEFAULT_PATH REQUIRED) find_path(QGIS_INCLUDE_DIR NAMES qgis.h PATHS "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/include" PATH_SUFFIXES qgis NO_DEFAULT_PATH ) find_library( QGIS_LIBRARY NAMES qgis_core PATHS "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/${qgis_debug_prefix}lib" PATH_SUFFIXES qgis NO_DEFAULT_PATH ) # because on the initial build the Qt toolchain file is not yet generated and therefore not included by qgis-js.cmake # this will ensure that the Qt toolchain file is included after qtbase is built if(EXISTS ${QT_TOOLCHAIN_FILE}) set(QT_CHAINLOAD_TOOLCHAIN_FILE ${EMSCRIPTEN_TOOLCHAIN_FILE}) include(${QT_TOOLCHAIN_FILE}) else() message(FATAL_ERROR "Could not find Qt toolchain file: ${QT_TOOLCHAIN_FILE}") endif() # since Qt 6.3 qt_standard_project_setup should be used so set some default values qt_standard_project_setup() set(QGISJS_SOURCES src/qgis-js.cpp src/api/QgisApi.cpp ) # this creates also .html + qtloader.js + adds various flags to the build qt_add_executable(qgis-js MANUAL_FINALIZATION ${QGISJS_SOURCES}) target_compile_options(qgis-js PRIVATE "-fwasm-exceptions") target_link_options(qgis-js PRIVATE "-fwasm-exceptions") # TODO reenable -msimd128 # target_compile_options(qgis-js PRIVATE "-msimd128") # target_link_options(qgis-js PRIVATE "-msimd128") target_include_directories(qgis-js PRIVATE ${QGIS_INCLUDE_DIR}) target_link_libraries(qgis-js PRIVATE Qt6::Xml Qt6::Concurrent Qt6::Network Qt6::Core Qt6::Gui Qt6::Core5Compat Qt6::PrintSupport Qt6::Widgets # because QgsApplication -> QApplication ) set(QGIS_PROVIDERS_LIST provider_arcgisfeatureserver provider_arcgismapserver provider_delimitedtext provider_wms provider_wcs ) foreach (provider ${QGIS_PROVIDERS_LIST}) find_library( QGIS_${provider}_LIBRARY NAMES ${provider}_a PATHS "${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/${qgis_debug_prefix}lib" PATH_SUFFIXES qgis NO_DEFAULT_PATH ) target_link_libraries(qgis-js PRIVATE ${QGIS_${provider}_LIBRARY} ) endforeach () # qgis_core must come after providers (they depend on it) target_link_libraries(qgis-js PRIVATE ${QGIS_LIBRARY}) target_link_libraries(qgis-js PRIVATE PROJ::proj) target_link_libraries(qgis-js PRIVATE unofficial::sqlite3::sqlite3) target_link_libraries(qgis-js PRIVATE GEOS::geos_c) target_link_libraries(qgis-js PRIVATE GDAL::GDAL) target_link_libraries(qgis-js PRIVATE expat::expat) target_link_libraries(qgis-js PRIVATE protobuf::libprotobuf-lite) target_link_libraries(qgis-js PRIVATE ${SPATIALINDEX_LIBRARY}) target_link_libraries(qgis-js PRIVATE libzip::zip) target_link_libraries(qgis-js PRIVATE Qt6Keychain::Qt6Keychain) target_link_libraries(qgis-js PRIVATE $,zstd::libzstd_shared,zstd::libzstd_static>) qt_finalize_executable(qgis-js) # # emcc settings (see https://emsettings.surma.technology/) # # NOTE: We set our flags after qt_finalize_executable in order to override the flags set by Qt # # Flags set by Qt: # -s PTHREAD_POOL_SIZE=4 # -s INITIAL_MEMORY=50MB # -s EXPORTED_RUNTIME_METHODS=UTF16ToString,stringToUTF16,JSEvents,specialHTMLTargets,FS # -s MAX_WEBGL_VERSION=2 # -s FETCH=1 # -s WASM_BIGINT=1 # -s STACK_SIZE=5MB # -s MODULARIZE=1 # -s EXPORT_NAME=createQtAppInstance # -s ALLOW_MEMORY_GROWTH #- s ASYNCIFY_IMPORTS=qt_asyncify_suspend_js,qt_asyncify_resume_js # -s ERROR_ON_UNDEFINED_SYMBOLS=1 # Emscripten Runtime target_link_options(qgis-js PRIVATE "SHELL: \ -s EXPORT_ES6" ) # FS (see https://emscripten.org/docs/api_reference/Filesystem-API.html) target_link_options(qgis-js PRIVATE "SHELL: \ -s FORCE_FILESYSTEM=1" ) # Threading (see https://emscripten.org/docs/porting/pthreads.html) set(MINIMAL_THREAD_POOL_SIZE "4") set(MAXIMAL_THREAD_POOL_SIZE "16") target_link_options(qgis-js PRIVATE "SHELL: \ -s PTHREAD_POOL_SIZE=\"Math.min(Math.max(((navigator&&navigator.hardwareConcurrency)||${MINIMAL_THREAD_POOL_SIZE}),${MINIMAL_THREAD_POOL_SIZE}),${MAXIMAL_THREAD_POOL_SIZE})\" \ -s PTHREAD_POOL_SIZE_STRICT=2 \ -s PTHREAD_POOL_DELAY_LOAD=1 \ -s MALLOC=mimalloc" ) target_link_options(qgis-js PRIVATE "SHELL: \ --preload-file ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/share/proj@/proj \ --preload-file ${_VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/share/qgis/resources/srs.db@/qgis/resources/srs.db \ --preload-file ${CMAKE_CURRENT_SOURCE_DIR}/src/qt.conf@/qt.conf" ) # Non-release builds: optimize link step to reduce function count (237K+ causes V8 OOM in Chrome) # This only affects the post-link wasm-opt pass, not source-level compilation if(NOT CMAKE_BUILD_TYPE STREQUAL "Release") target_link_options(qgis-js PRIVATE "-O2") endif() # Debug build settings if (CMAKE_BUILD_TYPE STREQUAL "Debug") target_compile_options(qgis-js PRIVATE "-Og") # DWARF debug info (see https://developer.chrome.com/blog/faster-wasm-debugging/) target_link_options(qgis-js PRIVATE "-gseparate-dwarf" "-gdwarf-5" "-gsplit-dwarf" "-gpubnames") # Allow the wasm function table to grow dynamically (debug builds have more indirect call targets) target_link_options(qgis-js PRIVATE "SHELL:-s ALLOW_TABLE_GROWTH=1") endif() # TODO remove this fix (see https://github.com/emscripten-core/emscripten/issues/21844) target_link_options(qgis-js PRIVATE "SHELL: \ -s EXPORTED_FUNCTIONS=_main,__emscripten_thread_crashed,__embind_initialize_bindings" ) ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to qgis-js Thank you for considering contributing to qgis-js! We welcome all contributions, big or small 🙏 ## QGIS Code of Conduct Please note that for this project the [QGIS Code of Conduct](https://qgis.org/en/site/getinvolved/governance/codeofconduct/codeofconduct.html) also applies. ## Getting Started To get started with contributing, please follow these steps: 1. Fork the repository and clone it to your local machine. 2. See the [README](../README.md) for instructions on how to build the project. 3. Make your changes and test them locally. 4. Submit a pull request with your changes. ## Code Style Please 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`. ## Reporting Bugs If 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. ## Contact If you have any questions or concerns, please reach out as follows: - [qgis-js issues](https://github.com/qgis/qgis-js/issues) - [qgis-js discussions](https://github.com/qgis/qgis-js/discussions) - [QGIS Developers mailing list](https://lists.osgeo.org/mailman/listinfo/qgis-developer) > 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 ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS In addition, as a special exception, the QGIS Development Team gives permission to link the code of this program with the Qt library, including but not limited to the following versions (both free and commercial): Qt/Non-commercial Windows, Qt/Windows, Qt/X11, Qt/Mac, and Qt/Embedded (or with modified versions of Qt that use the same license as Qt), and distribute linked combinations including the two. You must obey the GNU General Public License in all respects for all of the code used other than Qt. If you modify this file, you may extend this exception to your version of the file, but you are not obligated to do so. If you do not wish to do so, delete this exception statement from your version. How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: README.md ================================================ # qgis-js **QGIS core ported to WebAssembly to run it on the web platform** Version: `4.0.0` (based on QGIS 4.0.0) [qgis-js Repository](https://github.com/qgis/qgis-js) | [qgis-js Website](https://qgis.github.io/qgis-js) > ⚠️🧪 **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))_ > 🌱👋 **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))_ ## About This 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). qgis-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)). Please 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)). > 📚 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 ## Packages | Package | Description | npm | | -------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | **[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) | | **[@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) | | **[@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) | ## Getting started | Example | Source code | StackBlitz | | ------------------------------------ | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 📐 **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) | | 🗺️ **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) | ## Compatibility A modern desktop browser is needed. At the moment we only support/test **Chromium-based browsers (>= 95)** and **Firefox (>= 100)** > 📚 See [docs/compatibility.md](docs/compatibility.md) for more details ## Features This project is a work in progress. Currently it provides the following features: - QGIS core (and its [dependencies](#libraries)) compiled to WebAssembly - JavaScript/TypeScript API to interact QGIS core - Loading of QGIS projects - Non-blocking rendering of QGIS maps/tiles to [ImageData](https://developer.mozilla.org/en-US/docs/Web/API/ImageData?retiredLocale=de) - Optional [OpenLayers](https://openlayers.org/) integration ## Limitations Compared to the native build of QGIS, there are various limitations: - The API surface is very limited at the moment - Network-based layers (e.g. WMS, WFS, WMTS, XYZ, COG, Vector Tiles) are not supported at the moment - No Python (PyQGIS) available - No Qt GUI provided - Some providers that need to communicate with a server using sockets will probably never work without proxies (e.g. PostgreSQL) ## How to build qgis-js > 💡 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). ### Install dependencies #### Install the following **system packages** (on Ubuntu 22.04): ``` sudo apt-get install pkg-config ninja-build flex bison ``` #### Install dependencies with pnpm: ``` npx pnpm install ``` > This will also invoke `./qgis-js.ts -v install` on "postinstall" which > > - downloads and installs emsdk in `build/emdsk` > - downloads and installs vcpkg in `build/vcpkg` > - boostraps vcpkg and downlaod the ports sources > > see also [`build/scripts/install.sh`](./build/scripts/install.sh) for manual installation ### Compile qgis-js (and its [dependencies](#libraries)) with Emscripten ``` npm run compile ``` > - Can also be ivoked with `compile:debug` or `compile:release`, see [Build types](#Build-types) > - Will take about 30 minutes on a modern machine to compile all the vcpkg ports during the first run... ☕ > - see also [`build/scripts/compile.sh`](./build/scripts/compile.sh) for manual compiltion ### Build `qgis-js` packages You want to compile with a `Release` [build type](#build-types) first ``` npm run compile:release ``` After successful compilation, you can build the packages with Vite: ``` npm run build ``` > see the [packages listed at the beginning of this README](#packages) ### Development You probably want to compile with a `Dev` or `Debug` [build type](#build-types) first ``` npm run compile:dev ``` Start a Vite development server: ``` npm run dev ``` Open your browser at http://localhost:5173 ## Libraries | Library | License | Links | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | **abseil** (20260107.1)
_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) | | **double-conversion** (3.4.0)
_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) | | **egl-registry** (2025-05-27)
_EGL API and Extension Registry_ | | [Website](https://github.com/KhronosGroup/EGL-Registry) - [Source code](https://github.com/KhronosGroup/EGL-Registry) | | **exiv2** (0.28.8)
_Image metadata library and tools_ | GPL-2.0-or-later | [Website](https://exiv2.org) - [Source code](https://github.com/Exiv2/exiv2) | | **expat** (2.7.4)
_XML parser library written in C_ | MIT | [Website](https://github.com/libexpat/libexpat) - [Source code](https://github.com/libexpat/libexpat) | | **freetype** (2.13.3)
_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) | | **gdal** (3.12.2)
_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) | | **geos** (3.14.1)
_Geometry Engine Open Source_ | LGPL-2.1-only | [Website](https://libgeos.org/) | | **inih** (62)
_Simple .INI file parser_ | BSD-3-Clause | [Website](https://github.com/benhoyt/inih) - [Source code](https://github.com/benhoyt/inih) | | **json-c** (0.18-20240915)
_A JSON implementation in C_ | MIT | [Website](https://github.com/json-c/json-c) - [Source code](https://github.com/json-c/json-c) | | **libb2** (0.98.1)
_C library providing BLAKE2b, BLAKE2s, BLAKE2bp, BLAKE2sp_ | | [Website](https://github.com/BLAKE2/libb2) - [Source code](https://github.com/BLAKE2/libb2) | | **libgeotiff** (1.7.4)
_Libgeotiff is an open source library on top of libtiff for reading and writing GeoTIFF information tags._ | MIT | [Website](https://github.com/OSGeo/libgeotiff) | | **libiconv** (1.18)
_iconv() text conversion._ | | [Website](https://www.gnu.org/software/libiconv/) | | **libjpeg-turbo** (3.1.3)
_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) | | **liblzma** (5.8.2)
_Compression library with an API similar to that of zlib._ | | [Website](https://tukaani.org/xz/) | | **libpng** (1.6.55)
_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) | | **libspatialindex** (2.0.0)
_C++ implementation of R\*-tree, an MVR-tree and a TPR-tree with C API._ | MIT | [Website](http://libspatialindex.github.com) | | **libzip** (1.11.4)
_A C library for reading, creating, and modifying zip archives._ | BSD-3-Clause | [Website](https://github.com/nih-at/libzip) | | **md4c** (0.5.2)
_MD4C is a C library providing a Markdown parser._ | MIT | [Website](https://github.com/mity/md4c) - [Source code](https://github.com/mity/md4c) | | **nlohmann-json** (3.12.0)
_JSON for Modern C++_ | MIT | [Website](https://github.com/nlohmann/json) - [Source code](https://github.com/nlohmann/json) | | **opengl-registry** (2026-01-26)
_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) | | **opengl** (2022-12-04)
_Open Graphics Library (OpenGL)[3][4][5] is a cross-language, cross-platform application programming interface (API) for rendering 2D and 3D vector graphics._ | | | | **pcre2** (10.47)
_Regular Expression pattern matching using the same syntax and semantics as Perl 5._ | BSD-3-Clause | [Website](https://github.com/PCRE2Project/pcre2) | | **proj** (9.7.1)
_PROJ library for cartographic projections_ | MIT | [Website](https://proj.org/) - [Source code](https://github.com/OSGeo/PROJ) | | **protobuf** (6.33.4)
_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) | | **qgis** (4.0.0)
_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) | | **qt5compat** (6.10.1)
_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/) | | **qtbase** (6.10.1)
_Qt Base (Core, Gui, Widgets, Network, ...)_ | | [Website](https://www.qt.io/) | | **qtkeychain** (0.14.3)
_(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) | | **qtmultimedia** (6.10.1)
_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/) | | **qtshadertools** (6.10.1)
_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/) | | **qtsvg** (6.10.1)
_Qt SVG provides classes for rendering and displaying SVG drawings in widgets and on other paint devices._ | | [Website](https://www.qt.io/) | | **qttools** (6.10.1)
_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/) | | **sqlite3** (3.51.2)
_SQLite is a software library that implements a self-contained, serverless, zero-configuration, transactional SQL database engine._ | blessing | [Website](https://sqlite.org/) | | **tiff** (4.7.1)
_A library that supports the manipulation of TIFF image files_ | libtiff | [Website](https://libtiff.gitlab.io/libtiff/) - [Source code](https://gitlab.com/libtiff/libtiff) | | **utf8-range** (6.33.4)
_Fast UTF-8 validation with Range algorithm (NEON+SSE4+AVX2)_ | MIT | [Website](https://github.com/protocolbuffers/protobuf) - [Source code](https://github.com/protocolbuffers/protobuf) | | **zlib** (1.3.1)
_A compression library_ | Zlib | [Website](https://www.zlib.net/) - [Source code](https://github.com/madler/zlib) | | **zstd** (1.5.7)
_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) | ## Build types ### `Dev` build type - Optimized for **fast link times** during development - Symbols are present (e.g. meaningful stack traces) - Enables some Emscripten assertions - No DWARF debug info - Empty `CMAKE_BUILD_TYPE` in CMake ### `Debug` build type - Optimized for **debugging** with DWARF in Chromium-based browsers - Includes symbols and DWARF debug info - Enables most Emscripten assertions - see [docs/debugging.md](docs/debugging.md) on how to get started - Will take much longer to build than the default `Dev` build type - `CMAKE_BUILD_TYPE=Debug` in CMake ### `Release` build type - Optimized for **performance and minimal package size** - No symbols, assertions or DWARF debug info - Minified JavaScript files - Will take much longer to build than the default `Dev` build type - `CMAKE_BUILD_TYPE=Release` in CMake ## Contributing Contributions welcome, see [CONTRIBUTING.md](CONTRIBUTING.md) for how to get started ## License [GNU General Public License v2.0](LICENSE) ================================================ FILE: build/actions/clean.ts ================================================ import { CommandLineAction } from "@rushstack/ts-command-line"; import { QgisJsOptions } from "./lib/QgisJsOptions"; import "zx/globals"; export class CleanAction extends CommandLineAction { private _options: QgisJsOptions; public constructor(options: QgisJsOptions) { super({ actionName: "clean", summary: "Clean qgis-js build tree", documentation: `Cleans emsdk, vcpkg and the qgis-js build tree in "build/wasm".`, }); this._options = options; } protected onExecuteAsync(): Promise { return new Promise(async (resolve) => { const v = this._options.verbose; $.verbose = this._options.verbose; if (v) console.log(`- cleaning build/emsdk`); await $`(cd build/emsdk && git clean -xfd)`; if (v) console.log(`\n- cleaning build/vcpkg`); await $`(cd build/vcpkg && git clean -xfd)`; if (v) console.log(`\n- cleaning build/wasm`); await $`(rm -rf build/wasm && mkdir build/wasm)`; if (v) console.log(`\n`); resolve(); }); } } ================================================ FILE: build/actions/compile.ts ================================================ import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { CommandLineAction, CommandLineChoiceParameter, CommandLineFlagParameter, } from "@rushstack/ts-command-line"; import { BuildType } from "./lib/BuildType"; import { QgisJsOptions } from "./lib/QgisJsOptions"; import "zx/globals"; const CMakeCacheFile = "build/wasm/CMakeCache.txt"; export class CompileAction extends CommandLineAction { private _options: QgisJsOptions; private _buildType: CommandLineChoiceParameter; private _debug: CommandLineFlagParameter; public constructor(options: QgisJsOptions) { super({ actionName: "compile", summary: "Compiles qgis-js", documentation: "Uses emsdk, vcpkg and CMake to configure and build qgis-js.", }); this._options = options; this._debug = this.defineFlagParameter({ parameterLongName: "--debug", parameterShortName: "-d", description: "Prints debug information during build (and runs the compilation single threaded)", }); this._buildType = this.defineChoiceParameter({ parameterLongName: "--builde-type", parameterShortName: "-t", description: "Specify the CMake build type", alternatives: ["Dev", "Release", "Debug"], environmentVariable: "QGIS_JS_BUILD_TYPE", defaultValue: "Dev" as BuildType, }); } protected onExecuteAsync(): Promise { return new Promise(async (resolve, reject) => { const v = this._options.verbose; $.verbose = true; const repo = join(dirname(fileURLToPath(import.meta.url)), "../.."); const buildType = (this._buildType.value || "Dev") as BuildType; const debug = this._debug.value || false; if (v) console.log("Build Type:", buildType); // check if CMakeCache.txt needs to be regenerated if (fs.existsSync(CMakeCacheFile)) { if (v) console.log( `"${CMakeCacheFile}" exists, checking if it has to be regenerated`, ); const lastBuild = await lastBuildType(); if (lastBuild === buildType) { if (v) console.log( `Build type has not changed, skipping regenerating of ${CMakeCacheFile}`, ); } else { if (v) console.log( `Build type has changed, regenerating ${CMakeCacheFile}`, ); await $`rm ${CMakeCacheFile}`; if (v) console.log(`Build type has changed, removing build artifacts`); const artifacts = await glob(["build/wasm/{qt*,qgis-js*}"]); if (artifacts.length > 0) await $`rm ${artifacts}`; } } else { if (v) console.log(`"${CMakeCacheFile}" does not exist`); } // set environment variables for CMake process.env.VCPKG_BINARY_SOURCES = "clear"; if (debug) { process.env.VERBOSE = "1"; process.env.EMCC_DEBUG = "1"; } // if vcpkg has installed its own cmake, use that, otherwise use the system cmake const cmake = await $`find . -iwholename './build/vcpkg/downloads/tools/cmake-*/*/bin/cmake' | grep bin/cmake || echo cmake`; // configure and build vcpgk dependencies try { await $`${cmake} \ -S . \ -B build/wasm \ -G Ninja \ -DCMAKE_TOOLCHAIN_FILE=${repo}/build/vcpkg/scripts/buildsystems/vcpkg.cmake \ -DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=${repo}/build/vcpkg-toolchains/qgis-js.cmake \ -DVCPKG_OVERLAY_TRIPLETS=./build/vcpkg-triplets \ -DVCPKG_OVERLAY_PORTS=./build/vcpkg-ports \ -DVCPKG_TARGET_TRIPLET=wasm32-emscripten-qt-threads \ -DCMAKE_BUILD_TYPE=${buildType !== "Dev" ? buildType : ""}`; // build await $`${cmake} --build build/wasm`; } catch (error) { reject(error); return; } resolve(); }); } } async function lastBuildType(): Promise { const CMakeCacheFileContents = fs.readFileSync(CMakeCacheFile, "utf-8"); if (!CMakeCacheFileContents || CMakeCacheFileContents.length === 0) return undefined; const match = CMakeCacheFileContents.match(/CMAKE_BUILD_TYPE:STRING=(\w+)/); return (match ? match[1] : "Dev") as BuildType; } ================================================ FILE: build/actions/install.ts ================================================ import { CommandLineAction } from "@rushstack/ts-command-line"; import { QgisJsOptions } from "./lib/QgisJsOptions"; import "zx/globals"; export class InstallAction extends CommandLineAction { private _options: QgisJsOptions; public constructor(options: QgisJsOptions) { super({ actionName: "install", summary: "Installs tools and downloads dependency sources", documentation: `Installs emsdk and vcpkg and then downlaods the source code of all ports with vcpkg.`, }); this._options = options; } protected onExecuteAsync(): Promise { return new Promise(async (resolve) => { const v = this._options.verbose; $.verbose = true; if (v) console.log(`- installing emsdk`); // ensure git submodule is initialized await $`git submodule update --init build/emsdk`; // read engine "emsdk" from package.json const emsdkVersion = JSON.parse(fs.readFileSync("package.json", "utf-8")) .engines.emsdk; if (!emsdkVersion || emsdkVersion === "") throw new Error(`"emsdk" version not found in package.json`); await $`(cd build/emsdk && ./emsdk install ${emsdkVersion} && ./emsdk activate ${emsdkVersion})`; if (v) console.log(`\n- installing vcpkg`); // ensure git submodule is initialized await $`git submodule update --init build/vcpkg`; // bootstrap vcpkg await $`./build/vcpkg/bootstrap-vcpkg.sh -disableMetrics`; if (v) console.log(`\n- running vcpkg install`); await $`./build/vcpkg/vcpkg install \ --x-install-root=build/wasm/vcpkg_installed \ --only-downloads \ --overlay-triplets=build/vcpkg-triplets \ --overlay-ports=build/vcpkg-ports \ --triplet wasm32-emscripten-qt-threads`; if (v) console.log(`\n`); resolve(); }); } } ================================================ FILE: build/actions/lib/BuildType.ts ================================================ export type BuildType = "Dev" | "Release" | "Debug"; ================================================ FILE: build/actions/lib/QgisJsOptions.ts ================================================ export interface QgisJsOptions { verbose: boolean; } ================================================ FILE: build/actions/libs.ts ================================================ import { CommandLineAction, CommandLineChoiceParameter, } from "@rushstack/ts-command-line"; import "zx/globals"; import { QgisJsOptions } from "./lib/QgisJsOptions"; export class LibsAction extends CommandLineAction { private _options: QgisJsOptions; private _output: CommandLineChoiceParameter; public constructor(options: QgisJsOptions) { super({ actionName: "libs", summary: "List the vcpkg libraries", documentation: `Generates a list of all vcpkg libraries with version and license.`, }); this._options = options; this._output = this.defineChoiceParameter({ parameterLongName: "--output", parameterShortName: "-o", description: "Specify the commands output type", alternatives: ["json", "markdown"], environmentVariable: "QGIS_JS_LIBS_OUTPUT", defaultValue: "json", }); } protected onExecuteAsync(): Promise { return new Promise(async (resolve) => { $.verbose = this._options.verbose; const vcpgkPortList = JSON.parse( "" + (await $`./build/vcpkg/vcpkg list \ --x-install-root=build/wasm/vcpkg_installed \ --overlay-triplets=build/vcpkg-triplets \ --overlay-ports=build/vcpkg-ports \ --triplet wasm32-emscripten-qt-threads \ --x-full-desc \ --x-json`), ); const custom = { qt6: { license: "LGPL-3.0", website: "https://www.qt.io/", source: "https://github.com/qt/qtbase", }, qgis: { license: "GPL-2.0", website: "https://www.qgis.org/", source: "https://github.com/qgis/QGIS", }, "qca-qt6": { license: "LGPL-2.1", website: "https://userbase.kde.org/QCA", source: "https://github.com/KDE/qca", }, } as any; const libs = await Promise.all( Object.entries(vcpgkPortList) .filter(([key]) => key.endsWith("wasm32-emscripten-qt-threads")) .map< Promise<{ name: string; version: string; description: string; website?: string; license?: string; source?: string; }> >( ([, value]) => new Promise(async (resolve) => { const vcpgkPort = value as any; const response = await fetch( `https://vcpkg.link/ports/${vcpgkPort["package_name"]}.json`, ); if (response.status === 200) { const vcpkgLink = await response.json(); resolve({ name: vcpgkPort["package_name"].replace(/-qt6$/, ""), version: vcpgkPort["version"], description: vcpgkPort["desc"][0] || "", website: vcpkgLink["homepage_href"], license: vcpkgLink["license"], source: vcpkgLink["repository"]?.["href"], }); } else { if (vcpgkPort["package_name"] in custom) { resolve({ name: vcpgkPort["package_name"].replace(/-qt6$/, ""), version: vcpgkPort["version"], description: vcpgkPort["desc"][0] || "", website: custom[vcpgkPort["package_name"]]["website"], license: custom[vcpgkPort["package_name"]]["license"], source: custom[vcpgkPort["package_name"]]["source"], }); } else { throw new Error("Could not find", vcpgkPort); } } }), ), ); if (this._output.value === "json") { console.log(JSON.stringify(libs, null, 2)); } else if (this._output.value === "markdown") { const { tsMarkdown } = await import("ts-markdown"); const rows = libs.map((lib) => ({ Library: `**${lib.name}**` + (lib.version ? ` (${lib.version})` : "") + (lib.description ? `
_${lib.description}_` : ""), License: lib.license || "", Links: [ ...(lib.website ? [`[Website](${lib.website})`] : []), ...(lib.source ? [`[Source code](${lib.source})`] : []), ].join(" - ") || "", })); const table = { table: { columns: [ { name: "Library" }, { name: "License" }, { name: "Links" }, ], rows, }, }; console.log(tsMarkdown([table])); } resolve(); }); } } ================================================ FILE: build/actions/size.ts ================================================ import type { BrotliCompress, Gzip } from "zlib"; import { CommandLineAction, CommandLineChoiceParameter, } from "@rushstack/ts-command-line"; import { QgisJsOptions } from "./lib/QgisJsOptions"; import "zx/globals"; export class SizeAction extends CommandLineAction { private _options: QgisJsOptions; private _output: CommandLineChoiceParameter; public constructor(options: QgisJsOptions) { super({ actionName: "size", summary: "Measure the size of qgis-js", documentation: `Generates a list of all qgis-js assets and checks the compression ratio with "gzip" and "brotli".`, }); this._options = options; this._output = this.defineChoiceParameter({ parameterLongName: "--output", parameterShortName: "-o", description: "Specify the commands output type", alternatives: ["json", "markdown"], environmentVariable: "QGIS_JS_SIZE_OUTPUT", defaultValue: "json", }); } protected onExecuteAsync(): Promise { return new Promise(async (resolve) => { $.verbose = this._options.verbose; const fs = await import("fs"); const zlib = await import("zlib"); function measureCompression( filePath: string, compressionStream: BrotliCompress | Gzip, ) { return new Promise((resolve, reject) => { let compressedSize = 0; const fileStream = fs.createReadStream(filePath); fileStream.pipe(compressionStream); compressionStream.on("data", (chunk) => { compressedSize += chunk.length; }); compressionStream.on("end", () => { resolve(compressedSize); }); fileStream.on("error", reject); compressionStream.on("error", reject); }); } function humanFileSize(size: number) { const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); return ( Number((size / Math.pow(1024, i)).toFixed(2)) + " " + ["B", "kB", "MB", "GB", "TB"][i] ); } function spaceSaving(originalSize: number, compressedSize: number) { return Math.round((1 - compressedSize / originalSize) * 100); } const distDir = "packages/qgis-js/dist"; const files = [ "qgis.js", "assets/wasm/qgis-js.js", "assets/wasm/qgis-js.data", "assets/wasm/qgis-js.wasm", ]; const filesSize = {} as { [key: string]: { bytes: number; bytesGzip: number; bytesBrotli: number; }; }; for (const filename of files) { const filePath = `${distDir}/${filename}`; if (fs.existsSync(filePath)) { const stat = fs.statSync(filePath); filesSize[filename] = { bytes: stat.size, bytesGzip: await measureCompression(filePath, zlib.createGzip()), bytesBrotli: await measureCompression( filePath, zlib.createBrotliCompress({ params: { // zlib.constants.BROTLI_DEFAULT_QUALITY is 11, which is too slow for stream compression, // so we use the defaul from Apache httpd which is 5 // see https://httpd.apache.org/docs/2.4/mod/mod_brotli.html#brotlicompressionquality [zlib.constants.BROTLI_PARAM_QUALITY]: 5, }, }), ), }; } } const total = Object.entries(filesSize).reduce( (acc: any, [, value]) => { acc.bytes += value.bytes; acc.bytesGzip += value.bytesGzip; acc.bytesBrotli += value.bytesBrotli; return acc; }, { bytes: 0, bytesGzip: 0, bytesBrotli: 0, }, ); if (this._output.value === "json") { console.log( JSON.stringify( { total, files: filesSize, }, null, 2, ), ); } else if (this._output.value === "markdown") { const { tsMarkdown } = await import("ts-markdown"); console.log( `The size of the package is **\`${humanFileSize( total.bytes, )}\`** (uncompressed) or \`${humanFileSize( total.bytesBrotli, )}\` Brotli compressed (${spaceSaving( total.bytes, total.bytesBrotli, )}% space saving) / \`${humanFileSize( total.bytesGzip, )}\` Gzip compressed (${spaceSaving( total.bytes, total.bytesGzip, )}% space saving)`, ); console.log(""); console.log("It consists of the following files:"); console.log(""); const rows = Object.entries(filesSize).map(([file, size]) => ({ "File name": `\`${file}\``, "Size (uncompressed)": `\`${humanFileSize(size.bytes)}\``, "Size (Brotli compressed)": `\`${humanFileSize( size.bytesBrotli, )}\` (${spaceSaving(size.bytes, size.bytesBrotli)}% space saving)`, "Size (Gzip compressed)": `\`${humanFileSize( size.bytesGzip, )}\` (${spaceSaving(size.bytes, size.bytesGzip)}% space saving)`, })); const table = { table: { columns: [ { name: "File name" }, { name: "Size (uncompressed)" }, { name: "Size (Brotli compressed)" }, { name: "Size (Gzip compressed)" }, ], rows, }, }; console.log(tsMarkdown([table])); } resolve(); }); } } ================================================ FILE: build/scripts/clean.sh ================================================ #!/bin/bash set -eo pipefail (cd build/emsdk; git clean -xfd) (cd build/vcpkg; git clean -xfd) rm -rf build/wasm && mkdir build/wasm ================================================ FILE: build/scripts/compile.sh ================================================ #!/bin/bash set -eo pipefail # if vcpkg has installed its own cmake, use that, otherwise use the system cmake $(find . -iwholename './build/vcpkg/downloads/tools/cmake-*/*/bin/cmake' | grep bin/cmake || echo cmake) \ -S . \ -B build/wasm \ -G Ninja \ -DCMAKE_TOOLCHAIN_FILE=$PWD/build/vcpkg/scripts/buildsystems/vcpkg.cmake \ -DVCPKG_CHAINLOAD_TOOLCHAIN_FILE=$PWD/build/vcpkg-toolchains/qgis-js.cmake \ -DVCPKG_TARGET_TRIPLET=wasm32-emscripten-qt-threads \ -DCMAKE_BUILD_TYPE= $(echo ./build/vcpkg/downloads/tools/cmake-*/*/bin/cmake) \ --build build/wasm ================================================ FILE: build/scripts/install.sh ================================================ #!/bin/bash set -eo pipefail git submodule update --init build/emsdk build/emsdk/emsdk install 5.0.2; build/emsdk/emsdk activate 5.0.2; git submodule update --init build/vcpkg ./build/vcpkg/bootstrap-vcpkg.sh -disableMetrics ./build/vcpkg/vcpkg install \ --x-install-root=build/wasm/vcpkg_installed \ --only-downloads \ --triplet wasm32-emscripten-qt-threads ================================================ FILE: build/vcpkg-ports/libspatialindex/portfile.cmake ================================================ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO libspatialindex/libspatialindex REF "${VERSION}" SHA512 a508a9ed4019641bdaaa53533505531f3db440b046a9c7d9f78ed480293200c51796c2d826a6bb9b4f9543d60bb0fef9e4c885ec3f09326cfa4d2fb81c1593aa HEAD_REF master ) vcpkg_cmake_configure( SOURCE_PATH "${SOURCE_PATH}" WINDOWS_USE_MSBUILD OPTIONS -DCMAKE_DEBUG_POSTFIX=d -DSIDX_BUILD_TESTS:BOOL=OFF ) vcpkg_cmake_install() vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/${PORT}) vcpkg_fixup_pkgconfig() vcpkg_copy_pdbs() #Debug file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") # Handle copyright file(INSTALL "${SOURCE_PATH}/COPYING" DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}" RENAME copyright) ================================================ FILE: build/vcpkg-ports/libspatialindex/vcpkg.json ================================================ { "name": "libspatialindex", "version": "2.0.0", "description": "C++ implementation of R*-tree, an MVR-tree and a TPR-tree with C API.", "homepage": "http://libspatialindex.github.com", "license": "MIT", "dependencies": [ { "name": "vcpkg-cmake", "host": true }, { "name": "vcpkg-cmake-config", "host": true }, "zlib" ] } ================================================ FILE: build/vcpkg-ports/qgis/portfile.cmake ================================================ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO qgis/QGIS REF 8d368f3efd5be7a2321ada8cb5663d8f478f7d0f SHA512 2d603d81ad2b37c809549ff4d05419adf7fb87e35500cdde3d8cda82b6cb8e5337552a4482b2a3a842fde81453383442bad93632070681e50502c12e57019036 HEAD_REF master ) file(REMOVE ${SOURCE_PATH}/cmake/FindQtKeychain.cmake) file(REMOVE ${SOURCE_PATH}/cmake/FindGDAL.cmake) file(REMOVE ${SOURCE_PATH}/cmake/FindGEOS.cmake) file(REMOVE ${SOURCE_PATH}/cmake/FindEXIV2.cmake) file(REMOVE ${SOURCE_PATH}/cmake/FindExpat.cmake) file(REMOVE ${SOURCE_PATH}/cmake/FindIconv.cmake) vcpkg_find_acquire_program(FLEX) vcpkg_find_acquire_program(BISON) vcpkg_find_acquire_program(PYTHON3) get_filename_component(PYTHON3_PATH ${PYTHON3} DIRECTORY) vcpkg_add_to_path(${PYTHON3_PATH}) vcpkg_add_to_path(${PYTHON3_PATH}/Scripts) set(PYTHON_EXECUTABLE ${PYTHON3}) # Core options (matching build.sh / wasm.yml) list(APPEND QGIS_OPTIONS -DFORCE_STATIC_LIBS:BOOL=ON) list(APPEND QGIS_OPTIONS -DENABLE_TESTS:BOOL=OFF) list(APPEND QGIS_OPTIONS -DNATIVE_CRSSYNC_BIN=/bin/true) # Disabled features (matching build.sh / wasm.yml) list(APPEND QGIS_OPTIONS -DWITH_GUI:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_DESKTOP:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_BINDINGS:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_3D:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_EXIV2:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_PDAL:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_DRACO:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_QTSERIALPORT:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_QTPOSITIONING:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_QTWEBENGINE:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_AUTH:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_SPATIALITE:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_ANALYSIS:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_QGIS_PROCESS:BOOL=OFF) # Additional disabled features for WASM/qgis-js list(APPEND QGIS_OPTIONS -DWITH_POSTGRESQL:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_GRASS7:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_CUSTOM_WIDGETS:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_SERVER:BOOL=OFF) # Note: WITH_EPT and WITH_COPC are NOT disabled - they use WITH_INTERNAL_LAZPERF list(APPEND QGIS_OPTIONS -DWITH_APIDOC:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_QUICK:BOOL=OFF) list(APPEND QGIS_OPTIONS -DWITH_INTERNAL_POLY2TRI=ON) list(APPEND QGIS_OPTIONS -DWITH_INTERNAL_SPATIALINDEX=OFF) list(APPEND QGIS_OPTIONS -DWITH_INTERNAL_LAZPERF=ON) # Point CMake to the installed Qt6, not the buildtrees list(APPEND QGIS_OPTIONS -DQt6_DIR=${CURRENT_INSTALLED_DIR}/share/Qt6) list(APPEND QGIS_OPTIONS -DCMAKE_PREFIX_PATH=${CURRENT_INSTALLED_DIR}) list(APPEND QGIS_OPTIONS -DQt6LinguistTools_DIR=${CURRENT_HOST_INSTALLED_DIR}/share/Qt6LinguistTools) list(APPEND QGIS_OPTIONS -DQT_LRELEASE_EXECUTABLE=${CURRENT_HOST_INSTALLED_DIR}/tools/Qt6/bin/lrelease) # QGIS likes to install auth and providers to different locations on each platform # let's keep things clean and tidy and put them at a predictable location list(APPEND QGIS_OPTIONS -DQGIS_PLUGIN_SUBDIR=lib) # By default QGIS installs includes into "include" on Windows and into "include/qgis" everywhere else # let's keep things clean and tidy and put them at a predictable location list(APPEND QGIS_OPTIONS -DQGIS_INCLUDE_SUBDIR=include/qgis) list(APPEND QGIS_OPTIONS -DWITH_INTERNAL_POLY2TRI=ON) vcpkg_configure_cmake( SOURCE_PATH ${SOURCE_PATH} #PREFER_NINJA OPTIONS ${QGIS_OPTIONS} OPTIONS_DEBUG ${QGIS_OPTIONS_DEBUG} OPTIONS_RELEASE ${QGIS_OPTIONS_RELEASE} ) vcpkg_install_cmake() file(GLOB QGIS_CMAKE_PATH ${CURRENT_PACKAGES_DIR}/*.cmake) if(QGIS_CMAKE_PATH) file(COPY ${QGIS_CMAKE_PATH} DESTINATION ${CURRENT_PACKAGES_DIR}/share/cmake/${PORT}) file(REMOVE_RECURSE ${QGIS_CMAKE_PATH}) endif() file(GLOB QGIS_CMAKE_PATH_DEBUG ${CURRENT_PACKAGES_DIR}/debug/*.cmake) if( QGIS_CMAKE_PATH_DEBUG ) file(REMOVE_RECURSE ${QGIS_CMAKE_PATH_DEBUG}) endif() file(REMOVE_RECURSE ${CURRENT_PACKAGES_DIR}/debug/include ) file(REMOVE_RECURSE # Added for debug porpose ${CURRENT_PACKAGES_DIR}/debug/share ) # Handle copyright file(INSTALL ${SOURCE_PATH}/COPYING DESTINATION ${CURRENT_PACKAGES_DIR}/share/${PORT} RENAME copyright) ================================================ FILE: build/vcpkg-ports/qgis/vcpkg.json ================================================ { "name": "qgis", "version": "4.0.0", "port-version": 1, "homepage": "https://qgis.org", "description": "QGIS is a free, open source, cross platform (lin/win/mac) geographical information system (GIS)", "dependencies": [ "expat", "zlib", "zstd", "libspatialindex", { "name": "sqlite3", "default-features": false }, "exiv2", "protobuf", { "name": "proj", "default-features": false }, "geos", { "name": "gdal", "default-features": false }, { "name": "qtbase", "default-features": false, "features": [ "concurrent", "gui", "jpeg", "network", "png", "widgets", "sql", "sql-sqlite" ] }, { "name": "qt5compat", "default-features": false }, { "name": "qtkeychain-qt6", "default-features": false }, { "name": "qtsvg", "default-features": false }, { "name": "qtmultimedia", "default-features": false }, { "name": "qttools", "default-features": false, "features": [ "linguist" ] }, { "name": "qttools", "host": true, "default-features": false, "features": [ "linguist" ] }, { "name": "vcpkg-cmake", "host": true }, { "name": "vcpkg-cmake-config", "host": true } ] } ================================================ FILE: build/vcpkg-toolchains/qgis-js.cmake ================================================ message(STATUS "Using 'qgis-js' toolchain") # Setup EMSDK/EMSCRIPTEN_ROOT if(NOT DEFINED ENV{EMSDK}) get_filename_component(QGIS_JS_BUILD_EMSDK "${CMAKE_CURRENT_LIST_DIR}/../emsdk" ABSOLUTE) set(ENV{EMSDK} ${QGIS_JS_BUILD_EMSDK}) endif() if(NOT EMSCRIPTEN_ROOT) if(NOT DEFINED ENV{EMSDK}) message(FATAL_ERROR "emsdk not found") endif() set(EMSCRIPTEN_ROOT "$ENV{EMSDK}/upstream/emscripten") endif() if(NOT DEFINED ENV{EMSCRIPTEN_ROOT}) set(ENV{EMSCRIPTEN_ROOT} ${EMSCRIPTEN_ROOT}) endif() # Set EMSCRIPTEN_TOOLCHAIN_FILE for use by CMakeLists.txt set(EMSCRIPTEN_TOOLCHAIN_FILE "${EMSCRIPTEN_ROOT}/cmake/Modules/Platform/Emscripten.cmake") # Include Emscripten toolchain directly include("${EMSCRIPTEN_TOOLCHAIN_FILE}") # Set QT_TOOLCHAIN_FILE path for CMakeLists.txt to include if(CMAKE_TOOLCHAIN_FILE) get_filename_component(VCPKG_ROOT_DIR ${CMAKE_TOOLCHAIN_FILE} DIRECTORY) get_filename_component(VCPKG_ROOT_DIR ${VCPKG_ROOT_DIR} DIRECTORY) get_filename_component(VCPKG_PACKAGES_PATH "${VCPKG_ROOT_DIR}/../packages" ABSOLUTE) set(QT_TOOLCHAIN_FILE "${VCPKG_PACKAGES_PATH}/qtbase_${VCPKG_TARGET_TRIPLET}/share/Qt6/qt.toolchain.cmake") endif() # Flags for all ports and qgis-js # TODO reenable -msimd128 set(QGIS_JS_FLAGS "-pthread -fwasm-exceptions -sSUPPORT_LONGJMP=wasm") set(CMAKE_C_FLAGS_INIT "${CMAKE_C_FLAGS_INIT} ${QGIS_JS_FLAGS}") set(CMAKE_CXX_FLAGS_INIT "${CMAKE_CXX_FLAGS_INIT} ${QGIS_JS_FLAGS}") ================================================ FILE: build/vcpkg-triplets/wasm32-emscripten-qt-threads.cmake ================================================ message(STATUS "Using 'wasm32-emscripten-qt-threads' triplet") set(VCPKG_TARGET_ARCHITECTURE wasm32) set(VCPKG_CRT_LINKAGE dynamic) set(VCPKG_LIBRARY_LINKAGE static) set(VCPKG_CMAKE_SYSTEM_NAME Emscripten) # to avoid building both debug and release of all libs uncomment the following line # set(VCPKG_BUILD_TYPE "release") set(VCPKG_ENV_PASSTHROUGH_UNTRACKED EMSDK EMSCRIPTEN EMSCRIPTEN_ROOT PATH) # Tell autoconf configure scripts this is cross-compilation (fixes error 77) set(VCPKG_MAKE_BUILD_TRIPLET "--host=wasm32-unknown-emscripten") # this needs to be present for vcpkg installs, but also the same VCPKG_CHAINLOAD_TOOLCHAIN_FILE # needs to be present when running CMake so that the project gets it get_filename_component(QGISJS_TOOLCHAIN_FILE "${CMAKE_CURRENT_LIST_DIR}/../vcpkg-toolchains/qgis-js.cmake" ABSOLUTE) set(VCPKG_CHAINLOAD_TOOLCHAIN_FILE ${QGISJS_TOOLCHAIN_FILE}) set(VCPKG_ENV_PASSTHROUGH_UNTRACKED EMSDK EMSCRIPTEN EMSCRIPTEN_ROOT PATH) ================================================ FILE: build/vite/CrossOriginIsolationPlugin.ts ================================================ import type { Plugin } from "vite"; export const CrossOriginIsolationResponseHeaders = { "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", }; /** * A Vite plugin that adds Cross-Origin Isolation headers to the server responses. */ export default function CrossOriginIsolationPlugin(): Plugin { return { name: "CrossOriginIsolationPlugin", configureServer(server) { server.middlewares.use((_req, res, next) => { Object.entries(CrossOriginIsolationResponseHeaders).forEach( ([key, value]) => { res.setHeader(key, value); }, ); next(); }); }, }; } ================================================ FILE: build/vite/DirectoryListingPlugin.ts ================================================ import { File, Folder } from "../../packages/qgis-js-utils"; import type { Plugin, ResolvedConfig } from "vite"; import { basename, resolve, join } from "path"; import { readdir } from "fs/promises"; import { Dirent } from "fs"; const FILTER_LIST = [".DS_Store", ".git", ".gitignore", ".env"]; const DIRECTORY_LISTING_FILENAME = "directory-listing.json"; let config: ResolvedConfig; export default function DirectoryListingPlugin( directories: string | string[], ): Plugin { const directoriesToList = typeof directories === "string" ? [directories] : directories; for (const directory of directoriesToList) { if (!directory.startsWith("public/")) { throw new Error( `DirectoryListingPlugin: Directory ${directory} is not in the public folder`, ); } } return { name: "DirectoryListingPlugin", configResolved(_config) { config = _config; }, configureServer(server) { const dirs = directoriesToList.map( (directory) => config.base + directory.replace(/^public\//, "") + `/${DIRECTORY_LISTING_FILENAME}`, ); server.middlewares.use(async (req, res, next) => { if (req.url && dirs.includes(req.url)) { let dir = req.url; // remove potential base from start of the url if (dir.startsWith(config.base)) { dir = dir.slice(config.base.length); } // remove filename from url at the end dir = dir.slice(0, -`/${DIRECTORY_LISTING_FILENAME}`.length); const dirParts = dir.split("/"); const dirBase = dirParts.slice(0, -1).join("/"); const dirName = dirParts[dirParts.length - 1]; const drectoryListing = await createDirectoryListing( join(process.cwd(), "public", dirBase), dirName, ".", ); res.setHeader("Content-Type", "application/json"); res.end(JSON.stringify(drectoryListing, null, 2)); } else { next(); } }); }, async generateBundle() { for (const dir of directoriesToList.map((directory) => directory.replace(/^public\//, ""), )) { const drectoryListing = await createDirectoryListing( join(process.cwd(), "public"), dir, ".", ); this.emitFile({ type: "asset", fileName: dir + `/${DIRECTORY_LISTING_FILENAME}`, source: JSON.stringify(drectoryListing, null, 2), }); } }, }; } async function readFolder(folder: string): Promise { return await readdir(folder, { withFileTypes: true }); } async function createDirectoryListing( root: string, base: string, directory: string, ) { const directoryListing = await readFolder(join(root, base, directory)); return directoryListing .filter((dirent) => dirent.isFile || dirent.isDirectory) .filter((dirent) => !FILTER_LIST.includes(dirent.name)) .reduce( async (currentFolderPromise, entry: Dirent) => { const currentFolder = (await currentFolderPromise) as Folder; if (entry.isDirectory()) { currentFolder.entries.push( await createDirectoryListing( root, join(base, directory), entry.name, ), ); } else { currentFolder.entries.push({ name: entry.name, path: join(currentFolder.path, entry.name), type: "File", } as File); } return currentFolder; }, Promise.resolve({ name: basename(resolve(join(base, directory))), path: join(base, directory), type: "Folder", entries: [], } as Folder), ); } ================================================ FILE: build/vite/QgisRuntimePlugin.ts ================================================ import { join, resolve, basename } from "path"; import { existsSync, readFileSync } from "fs"; import { dirname } from "path"; import { fileURLToPath } from "url"; import { CrossOriginIsolationResponseHeaders } from "./CrossOriginIsolationPlugin"; import type { Plugin, ResolvedConfig } from "vite"; const __dirname = dirname(fileURLToPath(import.meta.url)); const RUNTIME_JS = "js"; const RUNTIME_WASM = "wasm"; const RUNTIME_WASM_MAP = "wasm.map"; const RUNTIME_WASM_DEBUG = "wasm.debug.wasm"; const RUNTIME_DATA = "data"; export const BASE_DIR = "wasm"; export interface Runtime { name: string; outputDir: string; } function patchEmccJs(content: string): string { // prevent setting of read-only property "stack" in Firefox // (will throw an error in strict mode) const search = `e.stack = arr.join("\\n");`; return content.replaceAll( search, `if (! (navigator.userAgent.indexOf("Firefox") !== -1)) { ${search} }`, ); } export default function QgisRuntimePlugin(_runtime: Runtime | null): Plugin { let runtime: Runtime; if (_runtime === null) { throw new Error("QgisRuntimePlugin: No runtime specified"); } else { runtime = _runtime; } let config: ResolvedConfig; let runtimeDir = () => `${config.build.assetsDir}/${BASE_DIR}`; const repoRoot = resolve(__dirname, "../.."); function file(ending: string) { return `${runtime.name}.${ending}`; } const fileRuntimeJs = file(RUNTIME_JS); const fileRuntimeWasm = file(RUNTIME_WASM); const fileRuntimeWasmMap = file(RUNTIME_WASM_MAP); const fileRuntimeWasmDebug = file(RUNTIME_WASM_DEBUG); const fileRuntimeData = file(RUNTIME_DATA); const filesRuntime = [ fileRuntimeJs, fileRuntimeWasmMap, fileRuntimeWasmDebug, fileRuntimeWasm, fileRuntimeData, ]; return { name: "QgisRuntimePlugin", enforce: "pre", configResolved(_config) { config = _config; }, configureServer(server) { const filesRuntimeDir = runtimeDir(); const runtimeFiles = filesRuntime.map( (id) => `${config.base}${filesRuntimeDir ? filesRuntimeDir + "/" : ""}${id}`, ); server.middlewares.use((req, res, next) => { if ( req.url && runtimeFiles.some((runtimefile) => runtimefile === req.url) ) { const filePath = join(repoRoot, runtime.outputDir, basename(req.url)); if (existsSync(filePath)) { const raw = readFileSync(filePath); res.statusCode = 200; Object.entries(CrossOriginIsolationResponseHeaders).forEach( ([key, value]) => { res.setHeader(key, value); }, ); if ( filePath.endsWith("." + RUNTIME_WASM) || filePath.endsWith("." + RUNTIME_DATA) ) { if (filePath.endsWith("." + RUNTIME_WASM)) { res.setHeader("Content-Type", "application/wasm"); } res.end(raw); } else if (filePath.endsWith("." + RUNTIME_JS)) { const content = raw.toString(); res.setHeader("Content-Type", "application/javascript"); res.end(patchEmccJs(content)); } else if (filePath.endsWith("." + RUNTIME_WASM_MAP)) { const content = raw.toString(); res.setHeader("Content-Type", "application/json"); res.end(content); } } else { console.log("404", filePath); res.statusCode = 404; res.end(); } } else { next(); } }); }, generateBundle() { filesRuntime.forEach((id) => { const filesRuntimeDir = runtimeDir(); const filePath = join(repoRoot, runtime.outputDir, id); if (existsSync(filePath)) { const raw = readFileSync(filePath); let source = raw as Uint8Array; if (filePath.endsWith("." + RUNTIME_JS)) { const encoder = new TextEncoder(); const content = patchEmccJs(raw.toString()); source = encoder.encode(content); } this.emitFile({ fileName: `${filesRuntimeDir ? filesRuntimeDir + "/" : ""}${id}`, type: "asset", source, }); } }); }, }; } ================================================ FILE: docs/architecture.md ================================================ # Architecture ## qgis-js Repository ## qgis-js Build ## qgis-js Runtime ================================================ FILE: docs/bundling.md ================================================ # Bundling qgis-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. See 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. To 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**. ### Explicitly specifying the runtime location The 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). ```js const { api } = await qgis({ prefix: "/path/to/runtime/assets", }); ``` ## Examples ### qgis-js with [Vite](https://vitejs.dev/) One can use the [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy). For 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). ### qgis.js with [Webpack](https://webpack.js.org/) With Webpack one can use the [copy-webpack-plugin](https://www.npmjs.com/package/copy-webpack-plugin). Note that the COOP/COEP headers have to be set in the `webpack.config.js` (see [`compatibility.md`](./compatibility.md) for more information). ### Using qgis-js from a CDN An example of how to use qgis-js from a CDN (e.g. [jsDelivr](https://www.jsdelivr.com/)): ```html qgis-js ``` Note 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))). And also ensure that the HTML document has the correct COOP/COEP headers set (see [`compatibility.md`](./compatibility.md) for more information). ================================================ FILE: docs/ci.md ================================================ # CI/CD ================================================ FILE: docs/compatibility.md ================================================ # Compatibility ## Features qgis-js uses the following features which have to be supported by the JavaScript/WebAssembly runtime: - ES modules with [dynamic imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#dynamic_imports) - [SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) - [WebAssembly](https://developer.mozilla.org/en-US/docs/WebAssembly) - [WebAssembly Threads](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) - [WebAssembly Exception Handling](https://developer.mozilla.org/en-US/docs/WebAssembly/Exception_handling) ## COOP/COEP In 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. Alternativley 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. ## Supported Browsers The [features listed above are supported by the following browsers](https://caniuse.com/es6-module-dynamic-import,wasm,sharedarraybuffer,mdn-javascript_builtins_webassembly_exception,webworkers): - **Chromium based browsers (>= 95)** - **Firefox (>= 100)** - Safari (15.2, yet to be tested!) ### Mobile Browsers > 🚧 At the moment we don't support mobile browsers ## Supported JavaScript Runtimes > 🚧 At the moment only browsers are supported ================================================ FILE: docs/debugging.md ================================================ # Debugging ## Setup - Make sure that you have compiled qgis-js with the [`Debug` build type](../README.md#build-types) - DWARF debug info is only supported in Chromium based browsers (Chrome, Edge, Brave, ...) - 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/)) - Install the [C/C++ DevTools Support (DWARF)](https://chrome.google.com/webstore/detail/pdcpmagijalfljmkmjngeonclgbbannb) extension ## Links - [Debugging WebAssembly with modern tools](https://developer.chrome.com/blog/wasm-debugging-2020/), Chrome for Developers Blog, 2020 - [Debugging WebAssembly Faster](https://developer.chrome.com/blog/faster-wasm-debugging/), Chrome for Developers Blog, 2022 - [WASM Debugging with Emscripten and VSCode](https://floooh.github.io/2023/11/11/emscripten-ide.html), The Brain Dump, 2023 ================================================ FILE: docs/examples/qgis-js-example-api/index.html ================================================ qgis-js-example-api

📐 Using the qgis-js API example

Open the Console to see the result 🤓

================================================ FILE: docs/examples/qgis-js-example-api/main.js ================================================ const QGIS_JS_DIST = window.location.pathname + "node_modules/qgis-js/dist"; // loading the qgis-js library const { qgis, QGIS_JS_VERSION } = await import(QGIS_JS_DIST + "/qgis.js"); console.log(`qgis-js (v${QGIS_JS_VERSION})`); // booting the qgis-js runtime console.log(`- loading qgis-js`); const { api } = await qgis({ prefix: QGIS_JS_DIST + "/assets/wasm", }); console.log(`- qgis-js ready`); // qgis-js API example console.log(`- creating a rectangle`); const rect = new api.QgsRectangle(1, 2, 3, 4); console.log("-> " + printRect(rect)); console.log(`- scaling the rectangle`); rect.scale(5); console.log("-> " + printRect(rect)); console.log(`- getting the center of the rectangle`); const center = rect.center(); console.log(`-> Point: x: ${center.x}, y: ${center.y}`); function printRect(rect) { return `QgsRectangle: xMaximum: ${rect.xMaximum}, xMinimum: ${rect.xMinimum}, yMaximum: ${rect.yMaximum}, yMinimum: ${rect.yMinimum}`; } ================================================ FILE: docs/examples/qgis-js-example-api/package.json ================================================ { "name": "qgis-js-example-api", "private": true, "type": "module", "scripts": { "start": "vite preview --outDir ." }, "dependencies": { "qgis-js": "4.0.0" }, "devDependencies": { "vite": "^8.0.0" } } ================================================ FILE: docs/examples/qgis-js-example-api/vite.config.js ================================================ import { defineConfig } from "vite"; export default defineConfig({ preview: { headers: { "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", }, }, }); ================================================ FILE: docs/examples/qgis-js-example-ol/index.html ================================================ qgis-js-example-ol

🗺️ Minimal OpenLayers example

================================================ FILE: docs/examples/qgis-js-example-ol/main.js ================================================ import { qgis } from "qgis-js"; import { QgisCanvasDataSource } from "@qgis-js/ol"; import { Map, View } from "ol"; import ImageLayer from "ol/layer/Image"; import Projection from "ol/proj/Projection.js"; // start the qgis-js runtime const { api, fs } = await qgis({ prefix: "/assets/wasm", }); // prepare the upload directory const uploadDir = "/upload"; fs.mkdir(uploadDir); // fetch demo project and upload it to the runtime const source = "https://raw.githubusercontent.com/boardend/qgis-js-projects/main/demo/World%20GPKG"; const files = ["project.qgz", "world_map.gpkg"]; for (const file of files) { const url = `${source}/${file}`; const response = await fetch(url); const blob = await response.blob(); const buffer = await blob.arrayBuffer(); fs.writeFile(`${uploadDir}/${file}`, new Uint8Array(buffer)); } // load the uploaded project api.loadProject(`${uploadDir}/${files[0]}`); // create the ol map with the projection from the project const projection = new Projection({ code: api.srid(), units: "m", }); function createQgisLayer() { return new ImageLayer({ source: new QgisCanvasDataSource(api, { projection, }), }); } const map = new Map({ target: "map", layers: [createQgisLayer()], view: new View({ center: [0, 0], zoom: 2, projection, }), }); // create a dropdown with all map themes const mapThemes = api.mapThemes(); if (mapThemes.length > 0) { const themeContainer = document.createElement("div"); themeContainer.style.marginTop = "1em"; document.body.appendChild(themeContainer); themeContainer.appendChild(document.createTextNode("Map theme: ")); const select = document.createElement("select"); select.addEventListener("change", () => { if (select.value) { api.setMapTheme(select.value); map.getLayers().clear(); map.addLayer(createQgisLayer()); } }); themeContainer.appendChild(select); const currentTheme = api.getMapTheme(); const option = document.createElement("option"); option.value = ""; option.text = ""; if (!currentTheme) { option.selected = true; } select.appendChild(option); for (const theme of api.mapThemes()) { const option = document.createElement("option"); option.value = theme; option.text = theme; if (theme === currentTheme) { option.selected = true; } select.appendChild(option); } } ================================================ FILE: docs/examples/qgis-js-example-ol/package.json ================================================ { "name": "qgis-js-example-ol", "private": true, "type": "module", "scripts": { "start": "vite dev", "build": "vite build", "preview": "vite preview" }, "dependencies": { "ol": "^10.8.0", "@qgis-js/ol": "4.0.0", "qgis-js": "4.0.0" }, "devDependencies": { "vite": "^8.0.0", "vite-plugin-static-copy": "^3.2.0" } } ================================================ FILE: docs/examples/qgis-js-example-ol/style.css ================================================ @import "node_modules/ol/ol.css"; body { font-family: sans-serif; margin: 1em; padding: 1em; } #map { width: 800px; height: 600px; border: 1px solid grey; } ================================================ FILE: docs/examples/qgis-js-example-ol/vite.config.js ================================================ import { defineConfig } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; const headers = { "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", }; export default defineConfig({ build: { target: "esnext", }, plugins: [ { name: "headers-after-static-copy", configureServer(server) { server.middlewares.use((_req, res, next) => { Object.entries(headers).forEach(([key, value]) => { res.setHeader(key, value); }); next(); }); }, }, viteStaticCopy({ silent: true, targets: [ { src: "node_modules/qgis-js/dist/assets/wasm/**", dest: "assets/wasm", }, ], }), ], preview: { headers, }, }); ================================================ FILE: docs/performance.md ================================================ # Performance and Optimization ## Preface In 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 More information - [Performance Comparison (between version 0.0.3 and 0.0.5)](https://github.com/boardend/qgis-js-performance) - Documentation - [`./debugging.md`](./debugging.md) - [`./profiling.md`](./profiling.md) ## Deliverables - [x] [A performance measurement tool for qgis-js](../sites/performance/) - [x] [A report on the performance gain](https://github.com/boardend/qgis-js-performance) - [x] [A final conclusion](#conclusion) ## Conclusion **Approach**: - 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 - The status of each optimization is tracked in the [list](#optimizations) - Note that most of them have been implemented - 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 - It is used on its worst performing extent (whole of Switzerland) and used to render a full screen map (`1920x1080`) - The project was measured in two versions: - [`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 - [`aos-playground`](https://github.com/boardend/qgis-js-projects/tree/main/performance/aos-playground): A optimized version of the project - Added/Recalculate indices for all layers - Simplified geometries for all vector layers - Played with the project settings to achieve potential performance improvements - 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: - 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) > All changes in the scope of this project have been merged with [PR #38](https://github.com/qgis/qgis-js/pull/38) **Results**: - Rendering time could be reduced significantly between version `0.0.3` and `0.0.5`: - Chrome: `41.60%` - Firefox: `43.04%` - Chrome is performing better than Firefox - Ratio rendering time Chrome/Firefox: `0.47` - Difference between `aos-baseline`/`aos-playground` project is negligible - `100.38%`, `100.51%`, `93.66%`, `101.62%` > see [Performance Comparison (between version 0.0.3 and 0.0.5)](https://github.com/boardend/qgis-js-performance) > Compare the two versions in the browser: > > - \>= `0.0.5`: https://qgis.github.io/qgis-js/ > - `0.0.3`: https://boardend.github.io/qgis-js-baseline/ **Further steps**: - Implement the not yet implemented [optimizations](#optimizations) - Add the performance measurement tool to the GitHub page - Automate the performance measurement tool to run on every commit/release - Further [profile](./profiling.md) the performance of the application in order to find bottlenecks - Compare the performance of the WebAssembly build against the native build of QGIS ## Optimizations > 💡 **Legend**: Each potential optimization will be prioritized with one of the following labels: > > **Priority**: > > - 🟢 High priority: _Should definitively taken into account_ > - 🟡 Mid priority: _Would be nice to investigate_ > - 🔴 Low priority: _Probably out of scope for now_ > > **Status**: > > - [x] Implemented > - [ ] Open ### Toolchain/Dependencies - [x] 🟢 Update to latest emscripten version - currently `3.1.29` is used (more than a year old) - by anecdotal evidence, binaries compiled with the latest version are faster (latest LLVM) - [x] 🟢 Update to Qt version `6.6.1` - see [wasm improvements since `6.5.2`](https://github.com/qt/qtreleasenotes/tree/dev/qt) - building of Qt is needed anyway, in order to pass custom build flags (see below) - [x] 🟡 Update to vcpkg version `2024.02.14` (and update the packages with it) - [ ] 🔴 Update to latest QGIS version - Currently `3.32.1` is used - Latest would be `3.34.3` - 💬 Created an issue to track this: https://github.com/qgis/qgis-js/issues/39 ### Build/Setup - [x] 🟢 Internalize Qt build - This makes is possible to also apply optimizations also to the Qt build - Check if the Qt build configuration can be optimized - [x] 🟢 Review and tweak the build flags/settings provided to emscripten - `-flto` (Link Time Optimization) - see https://emscripten.org/docs/optimizing/Optimizing-Code.html - see https://emsettings.surma.technology/ - [x] 🟢 Add [SIMD support](https://webassembly.org/features/) - Make use of auto vectorization - Add SIMD support for libraries (e.g. GDAL) - see https://jeromewu.github.io/improving-performance-using-webassembly-simd-intrinsics/ - [x] 🟢 Ensure [Bulk memory operations](https://webassembly.org/features/) - Not sure if we already make use of this - See `-mbulk-memory` - 💬 Note that this is the default of newer Emscripten versions - [ ] 🟢 Check if new [WASMFS](https://emscripten.org/docs/api_reference/Filesystem-API.html#new-file-system-wasmfs) helps to improve FS performance - 💬 This was not working well with the Qt CMake setup - Needs more time to investigate - [x] 🟡 Check other potential [WebAssembly features](https://webassembly.org/features/) - See if we can make use of any other of features generally available - 💬 Didn't find anything that could benefit the current setup ### Architecture/Runtime - **QGIS** - [x] 🟢 Switch to `QgsMapRendererParallelJob` - [x] 🟢 Check if auto geometry simplification is configured and used - [x] 🟢 Check if `QgsMapRendererCache` can help to speed up redraws - 💬 This doesn't help with the general rending performance. But could improove e.g. layer toggling. Not in use at the moment - [x] 🟡 Check all `MapSettingsFlag`, `ProjectReadFlag`, etc. for potential tweaks - [x] 🟡 Have a look at the QGIS Server implementation for potential tweaks - **API** - [x] 🟢 Check if JS promises can be returned directly from the API - 💬 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 - [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)) - 💬 This is not possible with the current architecture (But the copy takes less then 5ms) - **Qt** - [ ] 🟢 Check performance impact of OutputImageFormat `Format_ARGB32` - Is it faster to render in `Format_ARGB32_Premultiplied` and convert to `Format_ARGB32` afterwards? - **sqlite** - [x] 🟢 Check if WAL/locking etc. can be disabled for rendering (read-only) - 💬 See the flags in `qgis-js.cpp` ### UI/UX - [x] **OpenLayers** - [x] 🟢 Check with the OL devs, if the current layer implementations are well designed - 💬 Checked with [Andreas Hocevar](https://github.com/ahocevar) and created this follow up issues: - 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 - [x] 🟢 Cancellation of pending render jobs - 💬 Implemented with the `QgisJobDataSource` - [x] 🟢 Find a way to cache the results in "canvas" mode (like in XYZ mode) - 💬 Not possible at the moment (only with tiling like here: https://openlayers.org/en/latest/examples/wms-custom-proj.html) - [x] 🟡 Is it possible to display immediate results (progressive rendering), before the job finishes - 💬 Implemented with the `QgisJobDataSource` - [ ] 🔴 **QML** - Compare the performance of `QgsQuickMapCanvasMap` with the performance and UX of the current OL solution ### QGIS Project - [x] 🟢 Optimize the project settings for fastest possible rendering - 💬 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. - [x] 🟢 Ensure indices are created and used for all vector layers - 💬 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)) - [ ] 🟡 Check if there are faster data formats than GeoPackage (e.g. FlatGeobuf) ================================================ FILE: docs/profiling.md ================================================ # Profiling ## qgis-js Performance Measurement Tool The [qgis-js Performance Measurement Tool](../sites/performance/) can be used to measure the performance of the qgis-js application in a reproducible way: ``` cd sites/performance npm run dev ``` 1. Boot the runtime 2. Load a project 3. Render a first dummy frame 4. Start the performance test ## Browsers ### Chrome The 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. ![Firefox Profiler](https://developer.chrome.com/static/docs/devtools/performance/image/the-results-the-profile-5d830d01508e2_2880.png) - 💡 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` - ⚠️ Note that the `Debug` build type is significantly slower than the `Dev` build type ### Firefox The [Firefox Profiler⁩](https://profiler.firefox.com/) can be used to profile the performance of the qgis-js application. ![Firefox Profiler](https://profiler.firefox.com/b45b29da558efa211628.jpg) - 💡 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` - ⚠️ Note that the `Debug` build type is significantly slower than the `Dev` build type ## Emscripten - Emscripten provies a [profiling guide](https://emscripten.org/docs/optimizing/Optimizing-Code.html#profiling) and some embeddable tools to profile the application: - `--cpuprofiler` - `--memoryprofiler` - `--threadprofiler` ## QGIS profiling - It would be nice to extend the qgis-js API in order to retrieve profiling information from the QGIS core - ⚠️ This is not yet implemented ================================================ FILE: package.json ================================================ { "name": "qgis-js-repo", "private": true, "version": "4.0.0", "license": "GPL-2.0-or-later", "homepage": "https://qgis.github.io/qgis-js/", "repository": "github:qgis/qgis-js", "bugs": { "url": "https://github.com/qgis/qgis-js/issues" }, "packageManager": "pnpm@10.32.1", "engines": { "node": "22.16.0", "pnpm": "10.32.1", "emsdk": "5.0.2", "vcpkg": "2025.12.12", "qt": "6.10.1", "qgis": "4.0.0" }, "type": "module", "scripts": { "postinstall": "./qgis-js.ts -v install", "update": "pnpm update -r --ignore-scripts", "clean": "./qgis-js.ts -v clean", "compile": "pnpm run compile:dev", "compile:dev": "./qgis-js.ts compile", "compile:debug": "./qgis-js.ts compile -t Debug", "compile:release": "./qgis-js.ts compile -t Release", "build": "pnpm -r --filter=./packages/** run build", "dev": "pnpm --filter @qgis-js/dev dev", "dev:build": "pnpm --filter @qgis-js/dev build", "dev:preview": "npm run dev:build && pnpm --filter @qgis-js/dev preview", "site": "vite dev", "deploy": "pnpm -r --filter=./sites/** run deploy", "publish": "pnpm publish -r --filter=./packages/qgis-js** --access=public", "lint": "npm run lint:prettier && npm run lint:clang-format", "lint:prettier": "npx prettier . --write", "lint:pretty-quick": "pretty-quick --staged", "lint:clang-format": "clang-format -i \"--glob=src/**/*.{cpp,hpp}\"" }, "devDependencies": { "pnpm": "10.32.1", "vite": "8.0.0", "vite-node": "6.0.0", "zx": "8.8.5", "@rushstack/ts-command-line": "5.3.3", "ts-markdown": "1.3.0", "@microsoft/api-extractor": "7.57.7", "@microsoft/api-documenter": "7.29.7", "prettier": "3.8.1", "pretty-quick": "4.2.2", "clang-format": "1.8.0" } } ================================================ FILE: packages/qgis-js/README.md ================================================ # qgis-js **QGIS core ported to WebAssembly to run it on the web platform** Version: `4.0.0` (based on QGIS 4.0.0) [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) [![qgis-js on npm](https://img.shields.io/npm/v/qgis-js)](https://www.npmjs.com/package/qgis-js) > ⚠️🧪 **Work in progress**! Currently this project is in public beta ## Description QGIS 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. See the [qgis-js repository](https://github.com/qgis/qgis-js) for more information about the project. ## Installation ```bash npm install -S qgis-js ``` ## Usage ```js import { qgis } from "qgis-js"; const { api } = await qgis(); const rect = new api.QgsRectangle(1, 2, 3, 4); rect.scale(5); const center = rect.center(); console.log(center.x, center.y); ``` > 💡 Have a look at the [Integration packages](#integration-packages) to load QGIS projects and display them on a map > ⚠️ It must be ensured that... > > - the clients meets the [compatibility requirements](https://github.com/qgis/qgis-js/blob/main/docs/compatibility.md) > - that the webserver is configured to [allow cross-origin requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) > - 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) ## Integration packages | Package | Description | npm | | -------------------------------------------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | **[@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) | | **[@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) | ## WebAssembly module ### Size The 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) It consists of the following files: | File name | Size (uncompressed) | Size (Brotli compressed) | Size (Gzip compressed) | | -------------------------- | ------------------- | ----------------------------- | ----------------------------- | | `qgis.js` | `5.19 kB` | `1.87 kB` (64% space saving) | `1.97 kB` (62% space saving) | | `assets/wasm/qgis-js.js` | `276.92 kB` | `61.28 kB` (78% space saving) | `65.16 kB` (76% space saving) | | `assets/wasm/qgis-js.data` | `13.4 MB` | `2.01 MB` (85% space saving) | `2.62 MB` (80% space saving) | | `assets/wasm/qgis-js.wasm` | `63.39 MB` | `16.67 MB` (74% space saving) | `19.4 MB` (69% space saving) | ### Libraries | Library | License | Links | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | **abseil** (20260107.1)
_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) | | **double-conversion** (3.4.0)
_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) | | **egl-registry** (2025-05-27)
_EGL API and Extension Registry_ | | [Website](https://github.com/KhronosGroup/EGL-Registry) - [Source code](https://github.com/KhronosGroup/EGL-Registry) | | **exiv2** (0.28.8)
_Image metadata library and tools_ | GPL-2.0-or-later | [Website](https://exiv2.org) - [Source code](https://github.com/Exiv2/exiv2) | | **expat** (2.7.4)
_XML parser library written in C_ | MIT | [Website](https://github.com/libexpat/libexpat) - [Source code](https://github.com/libexpat/libexpat) | | **freetype** (2.13.3)
_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) | | **gdal** (3.12.2)
_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) | | **geos** (3.14.1)
_Geometry Engine Open Source_ | LGPL-2.1-only | [Website](https://libgeos.org/) | | **inih** (62)
_Simple .INI file parser_ | BSD-3-Clause | [Website](https://github.com/benhoyt/inih) - [Source code](https://github.com/benhoyt/inih) | | **json-c** (0.18-20240915)
_A JSON implementation in C_ | MIT | [Website](https://github.com/json-c/json-c) - [Source code](https://github.com/json-c/json-c) | | **libb2** (0.98.1)
_C library providing BLAKE2b, BLAKE2s, BLAKE2bp, BLAKE2sp_ | | [Website](https://github.com/BLAKE2/libb2) - [Source code](https://github.com/BLAKE2/libb2) | | **libgeotiff** (1.7.4)
_Libgeotiff is an open source library on top of libtiff for reading and writing GeoTIFF information tags._ | MIT | [Website](https://github.com/OSGeo/libgeotiff) | | **libiconv** (1.18)
_iconv() text conversion._ | | [Website](https://www.gnu.org/software/libiconv/) | | **libjpeg-turbo** (3.1.3)
_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) | | **liblzma** (5.8.2)
_Compression library with an API similar to that of zlib._ | | [Website](https://tukaani.org/xz/) | | **libpng** (1.6.55)
_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) | | **libspatialindex** (2.0.0)
_C++ implementation of R\*-tree, an MVR-tree and a TPR-tree with C API._ | MIT | [Website](http://libspatialindex.github.com) | | **libzip** (1.11.4)
_A C library for reading, creating, and modifying zip archives._ | BSD-3-Clause | [Website](https://github.com/nih-at/libzip) | | **md4c** (0.5.2)
_MD4C is a C library providing a Markdown parser._ | MIT | [Website](https://github.com/mity/md4c) - [Source code](https://github.com/mity/md4c) | | **nlohmann-json** (3.12.0)
_JSON for Modern C++_ | MIT | [Website](https://github.com/nlohmann/json) - [Source code](https://github.com/nlohmann/json) | | **opengl-registry** (2026-01-26)
_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) | | **opengl** (2022-12-04)
_Open Graphics Library (OpenGL)[3][4][5] is a cross-language, cross-platform application programming interface (API) for rendering 2D and 3D vector graphics._ | | | | **pcre2** (10.47)
_Regular Expression pattern matching using the same syntax and semantics as Perl 5._ | BSD-3-Clause | [Website](https://github.com/PCRE2Project/pcre2) | | **proj** (9.7.1)
_PROJ library for cartographic projections_ | MIT | [Website](https://proj.org/) - [Source code](https://github.com/OSGeo/PROJ) | | **protobuf** (6.33.4)
_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) | | **qgis** (4.0.0)
_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) | | **qt5compat** (6.10.1)
_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/) | | **qtbase** (6.10.1)
_Qt Base (Core, Gui, Widgets, Network, ...)_ | | [Website](https://www.qt.io/) | | **qtkeychain** (0.14.3)
_(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) | | **qtmultimedia** (6.10.1)
_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/) | | **qtshadertools** (6.10.1)
_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/) | | **qtsvg** (6.10.1)
_Qt SVG provides classes for rendering and displaying SVG drawings in widgets and on other paint devices._ | | [Website](https://www.qt.io/) | | **qttools** (6.10.1)
_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/) | | **sqlite3** (3.51.2)
_SQLite is a software library that implements a self-contained, serverless, zero-configuration, transactional SQL database engine._ | blessing | [Website](https://sqlite.org/) | | **tiff** (4.7.1)
_A library that supports the manipulation of TIFF image files_ | libtiff | [Website](https://libtiff.gitlab.io/libtiff/) - [Source code](https://gitlab.com/libtiff/libtiff) | | **utf8-range** (6.33.4)
_Fast UTF-8 validation with Range algorithm (NEON+SSE4+AVX2)_ | MIT | [Website](https://github.com/protocolbuffers/protobuf) - [Source code](https://github.com/protocolbuffers/protobuf) | | **zlib** (1.3.1)
_A compression library_ | Zlib | [Website](https://www.zlib.net/) - [Source code](https://github.com/madler/zlib) | | **zstd** (1.5.7)
_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) | ## Versioning This 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). ## License [GNU General Public License v2.0](https://github.com/qgis/qgis-js/blob/main/LICENSE) ================================================ FILE: packages/qgis-js/package.json ================================================ { "name": "qgis-js", "version": "4.0.0", "description": "QGIS core ported to WebAssembly to run it on the web platform", "license": "GPL-2.0-or-later", "homepage": "https://qgis.github.io/qgis-js/", "repository": { "type": "git", "url": "https://github.com/qgis/qgis-js", "directory": "packages/qgis-js" }, "bugs": { "url": "https://github.com/qgis/qgis-js/issues" }, "type": "module", "main": "dist/qgis.js", "types": "dist/qgis.d.ts", "files": [ "dist/**/*" ], "scripts": { "build": "vite build" }, "dependencies": { "@types/emscripten": "1.41.5", "p-limit": "7.3.0" }, "devDependencies": { "@types/node": "^22.19.15", "typescript": "5.8.2", "vite": "8.0.0", "vite-plugin-dts": "4.5.4" }, "keywords": [ "qgis", "qgisjs", "qgis-js", "qgiswasm", "qgis-wasm", "webassembly", "wasm", "geo", "geospatial" ] } ================================================ FILE: packages/qgis-js/src/QgisApiAdapter.ts ================================================ import { InternalQgisApi, QgisApi, QgisApiAdapter, } from "../../../src/api/QgisApi"; import { QgsRectangle } from "../../../src/api/QgisModel"; import { threadPoolSize } from "./runtime"; import pLimit from "p-limit"; import type { LimitFunction } from "p-limit"; export class QgisApiAdapterImplementation implements QgisApiAdapter { private readonly _api: InternalQgisApi; private readonly _threadPoolSize: number; private readonly _limit: LimitFunction; constructor(api: InternalQgisApi) { this._api = api; this._threadPoolSize = threadPoolSize(); this._limit = pLimit(this._threadPoolSize); } protected runLimited(fn: () => Promise): Promise { return this._limit(fn); } renderImage( srid: string, extent: QgsRectangle, width: number, height: number, pixelRatio: number = window?.devicePixelRatio || 1, layerIds?: string[], ): Promise { return this.runLimited(() => { return new Promise((resolve) => { this._api.renderImage( srid, extent, width, height, pixelRatio, (tileData) => { const data = new Uint8ClampedArray( tileData, ) as Uint8ClampedArray; const imageData = new ImageData(data, width, height); resolve(imageData); }, layerIds, ); }); }); } renderXYZTile( x: number, y: number, z: number, tileSize: number = 256, pixelRatio: number = window?.devicePixelRatio || 1, extentBuffer: number = 0, layerIds?: string[], ): Promise { return this.runLimited(() => { return new Promise((resolve) => { this._api.renderXYZTile( x, y, z, tileSize, pixelRatio, extentBuffer, (tileData) => { const data = new Uint8ClampedArray( tileData, ) as Uint8ClampedArray; const imageData = new ImageData(data, tileSize, tileSize); resolve(imageData); }, layerIds, ); }); }); } mapThemes(): readonly string[] { const mapLayersRaw = this._api.mapThemes(); const result = new Array(mapLayersRaw.size()); for (let i = 0; i < mapLayersRaw.size(); i++) { result[i] = mapLayersRaw.get(i); } return result; } } export function getQgisApiProxy(api: InternalQgisApi): QgisApi { const adapter = new QgisApiAdapterImplementation(api); return new Proxy( // @ts-ignore {}, { has(_target, property) { return property in adapter || property in api; }, get(_target, property) { if (property in adapter) { // @ts-ignore return adapter[property]; } else if (property in api) { // @ts-ignore return api[property]; } }, }, ); } ================================================ FILE: packages/qgis-js/src/emscripten.ts ================================================ /// /** * Extension of a EmscriptenModule that adds additional properties */ export interface EmscriptenRuntimeModule extends EmscriptenModule { [x: string]: any; } /** * Emscripten file system * * {@link https://emscripten.org/docs/api_reference/Filesystem-API.html} */ export type EmscriptenFS = typeof FS; ================================================ FILE: packages/qgis-js/src/index.ts ================================================ /** * The version of qgis-js. */ export const QGIS_JS_VERSION: string = //@ts-ignore (will be defined by vite) __QGIS_JS_VERSION; export type { QgisApi, QgisApiAdapter, CommonQgisApi, InternalQgisApi, LayerDefinitionResult, } from "../../../src/api/QgisApi"; export type { EmscriptenFS } from "./emscripten"; export type { QgsPointXY, QgsRectangle, QgsLayerTreeModelLegendNode, QgsLayerTreeNode, QgsLayerTreeGroup, QgsLayerTreeLayer, QgsMapLayer, QgsVectorLayer, } from "../../../src/api/QgisModel"; export { LayerType, NodeType } from "../../../src/api/QgisModel"; export { qgis } from "./loader"; ================================================ FILE: packages/qgis-js/src/loader.ts ================================================ import { getQgisApiProxy } from "./QgisApiAdapter"; import { threadPoolSize } from "./runtime"; import type { QgisRuntime, QgisRuntimeConfig, QgisRuntimeModule, } from "./runtime"; import type { EmscriptenRuntimeModule } from "./emscripten"; /** * Emscripten module configuration */ interface QtAppConfig {} /** * Interface for a Emscripten module that creates a Qt app instance. */ interface QtRuntimeFactory { createQtAppInstance(config: QtAppConfig): Promise; } /** * Loads the QtRuntimeFactory Emscripten module with the given prefix. * * @param mainScriptPath - The import path of the main script * @returns A promise that resolves with the QtRuntimeFactory module. */ function loadModule(mainScriptPath: string): Promise { return new Promise(async (resolve, reject) => { try { // hack to import es module without vite knowing about it const createQtAppInstance = ( await new Function(`return import("${mainScriptPath}")`)() ).default; resolve({ createQtAppInstance, }); } catch (error) { reject(error); } }); } /** * Load and initialize a new qgis-js runtime. * * @param config The {@link QgisRuntimeConfig} that will be taken into account during loading and initialization. * @returns A promise that resolves to a {@link QgisRuntime}. */ export async function qgis( config: QgisRuntimeConfig = {}, ): Promise { return new Promise(async (resolve, reject) => { let prefix: string | undefined = undefined; if (config.prefix) { prefix = config.prefix; } else { const url = import.meta.url; if (/.*\/src\/loader\.[ts|js]\?*[^/]*$/.test(url)) { console.warn( [ `qgis-js loader is running in development mode and no "prefix" seems to be configured.`, ` - Consider adding the QgisRuntimePlugin when bundling with Vite.`, ` - For more information see: https://github.com/qgis/qgis-js/blob/main/docs/bundling.md`, ].join("\n"), ); if (typeof window !== "undefined") { prefix = new URL("assets/wasm", window.location.href).pathname; } } else { prefix = new URL(/* @vite-ignore */ "assets/wasm", import.meta.url) .href; } } if (!prefix) { prefix = "assets/wasm"; } else { prefix = prefix.replace(/\/$/, ""); // ensure no trailing slash } let qtRuntimeFactory: QtRuntimeFactory | undefined = undefined; try { const mainScriptPath = `${prefix}/qgis-js.js`; qtRuntimeFactory = await loadModule(mainScriptPath); } catch (error) { reject( new Error(`Unable to load the qgis-js.js script`, { cause: error }), ); return; } const { createQtAppInstance } = qtRuntimeFactory!; let canvas: HTMLDivElement | undefined = undefined; if (typeof document !== "undefined") { canvas = document?.querySelector("#screen") as HTMLDivElement; } const runtimePromise = createQtAppInstance({ locateFile: (path: string) => `${prefix}/` + path, preRun: [ function (module: any) { module.qtContainerElements = canvas ? [canvas] : []; module.qtFontDpi = 96; module.qgisJsMaxThreads = threadPoolSize(); }, ], postRun: [ async function () { const runtime = await runtimePromise; resolve({ api: getQgisApiProxy(runtime), module: runtime as EmscriptenRuntimeModule, fs: runtime.FS, }); }, ], ...(config.onStatus ? { setStatus: config.onStatus } : {}), }); }); } ================================================ FILE: packages/qgis-js/src/runtime.ts ================================================ import { EmscriptenRuntimeModule, EmscriptenFS } from "./emscripten"; import { QgisApi, InternalQgisApi } from "../../../src/api/QgisApi"; /** * Qt emscripten runtime module that exposes the QgisInternalApi */ export interface QgisRuntimeModule extends EmscriptenRuntimeModule, InternalQgisApi {} /** * Boot configuration options for the QGIS runtime. */ export interface QgisRuntimeConfig { /** * The prefix to use for the {@link EmscriptenRuntimeModule} path. */ prefix?: string; /** * A callback function that will be called when the runtime status changes. */ onStatus?: (status: string) => void; } /** * Wraps the {@link EmscriptenRuntimeModule} and exposes the {@link QgisApi} and {@link EmscriptenFS} */ export interface QgisRuntime { api: QgisApi; module: EmscriptenRuntimeModule; fs: EmscriptenFS; } /** * Returns the thread pool size based on the hardware concurrency of the user's device. * The thread pool size is capped between a minium of 4 and a maximum 16 threads. * * @privateRemarks This needs to be in sync with PTHREAD_POOL_SIZE in CMakeLists.txt * * @returns The thread pool size of the qgis-js runtime */ export function threadPoolSize() { const MINIMAL_THREAD_POOL_SIZE = 4; const MAXIMAL_THREAD_POOL_SIZE = 16; return Math.min( Math.max( navigator?.hardwareConcurrency || MINIMAL_THREAD_POOL_SIZE, MINIMAL_THREAD_POOL_SIZE, ), MAXIMAL_THREAD_POOL_SIZE, ); } ================================================ FILE: packages/qgis-js/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: packages/qgis-js/vite.config.ts ================================================ import { resolve } from "path"; import { defineConfig } from "vite"; import QgisRuntimePlugin from "../../build/vite/QgisRuntimePlugin"; import dts from "vite-plugin-dts"; import packageJson from "./package.json"; export default defineConfig({ define: { __QGIS_JS_VERSION: JSON.stringify(packageJson.version), }, plugins: [ QgisRuntimePlugin({ name: "qgis-js", outputDir: "build/wasm", }), dts({ copyDtsFiles: true, staticImport: true, // insertTypesEntry: true, compilerOptions: { declarationMap: true, }, rollupTypes: true, entryRoot: "src", rollupConfig: { docModel: { enabled: true, apiJsonFilePath: resolve(__dirname, "etc/qgis-js.api.json"), }, }, async afterBuild() { // remove empty export statement const fs = await import("fs"); const path = await import("path"); const dtsFile = path.join(__dirname, "dist", "qgis.d.ts"); let content = fs.readFileSync(dtsFile, "utf-8"); content = content.replace("export { }", ""); // format file with prettier const prettier = await import("prettier"); const prettierConfig = await prettier.resolveConfig( path.join(__dirname, "../..", ".prettierrc.json"), ); content = await prettier.format(content, { ...prettierConfig, parser: "typescript", }); fs.writeFileSync(dtsFile, content); return; }, }), ], build: { lib: { entry: [resolve(__dirname, "src/index.ts")], name: "qgis-js", formats: ["es"], fileName: "qgis", }, }, }); ================================================ FILE: packages/qgis-js-ol/README.md ================================================ # @qgis-js/ol **OpenLayers sources for [qgis-js](https://github.com/qgis/qgis-js)** [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) [![@qgis-js/ol on npm](https://img.shields.io/npm/v/@qgis-js/ol)](https://www.npmjs.com/package/@qgis-js/ol) > ⚠️🧪 **Work in progress**! Currently this project is in public beta ## Prerequisites - This package requires **[OpenLayers](https://openlayers.org) `>=8`** to be installed as a peer dependency - The [qgis-js](https://www.npmjs.com/package/@qgis-js/ol) package is also required as a direct dependency of this package - An instance of the qgis-js runtime has to be created at runtime and its API must be passed to the OpenLayers source constructor ## Installation ```bash npm install -S @qgis-js/ol ``` ## Usage ### QgisCanvasDataSource [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. > See [QgisCanvasDataSource.ts](https://github.com/qgis/qgis-js/blob/main/packages/qgis-js-ol/src/QgisCanvasDataSource.ts) for the implementation. ### QgisXYZDataSource [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. > See [QgisXYZDataSource.ts](https://github.com/qgis/qgis-js/blob/main/packages/qgis-js-ol/src/QgisXYZDataSource.ts) for the implementation. ## Versioning This 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). ## License [GNU General Public License v2.0](https://github.com/qgis/qgis-js/blob/main/LICENSE) ================================================ FILE: packages/qgis-js-ol/package.json ================================================ { "name": "@qgis-js/ol", "version": "4.0.0", "description": "OpenLayers sources for qgis-js", "license": "GPL-2.0-or-later", "homepage": "https://qgis.github.io/qgis-js/", "repository": { "type": "git", "url": "https://github.com/qgis/qgis-js", "directory": "packages/qgis-js-ol" }, "bugs": { "url": "https://github.com/qgis/qgis-js/issues" }, "type": "module", "main": "dist/qgis-js-ol.js", "types": "dist/qgis-js-ol.d.ts", "files": [ "dist/**/*" ], "scripts": { "build": "vite build" }, "dependencies": { "qgis-js": "workspace:*" }, "peerDependencies": { "ol": "^10.8.0" }, "devDependencies": { "@types/node": "^22.19.15", "typescript": "5.8.2", "vite": "8.0.0", "vite-plugin-dts": "4.5.4" } } ================================================ FILE: packages/qgis-js-ol/src/QgisCanvasDataSource.ts ================================================ import type { QgisApi } from "qgis-js"; import ImageSource, { Options } from "ol/source/Image"; import { getWidth, getHeight } from "ol/extent"; export interface QgisCanvasDataSourceOptions extends Options { layerIds?: string[]; renderFunction?: QgisCanvasRenderFunction; } export type QgisCanvasRenderFunction = ( api: QgisApi, srid: string, xMin: number, yMin: number, xMax: number, yMax: number, width: number, height: number, pixelRatio: number, layerIds?: string[], ) => Promise; export class QgisCanvasDataSource extends ImageSource { protected api: QgisApi; protected static DEFAULT_RENDERFUNCTION: QgisCanvasRenderFunction = ( api: QgisApi, srid: string, xMin: number, yMin: number, xMax: number, yMax: number, width: number, height: number, pixelRatio: number, layerIds?: string[], ) => { return api.renderImage( srid, new api.QgsRectangle(xMin, yMin, xMax, yMax), width, height, pixelRatio, layerIds, ); }; protected renderFunction: QgisCanvasRenderFunction | undefined; protected layerIds: string[] | undefined; protected getrenderFunction(): QgisCanvasRenderFunction { return this.renderFunction || QgisCanvasDataSource.DEFAULT_RENDERFUNCTION; } constructor(api: QgisApi, options: QgisCanvasDataSourceOptions = {}) { super({ loader: (extent, resolution, requestPixelRatio) => { return new Promise(async (resolve) => { // note: requestPixelRatio is managed by ol and will not change on zoom const pixelRatio = requestPixelRatio || window?.devicePixelRatio || 1; const imageResolution = resolution / pixelRatio; const width = Math.round(getWidth(extent) / imageResolution); const height = Math.round(getHeight(extent) / imageResolution); const renderFunction = this.getrenderFunction(); const imageData = await renderFunction( this.api, this.getProjection()?.getCode() || "EPSG:3857", extent[0], extent[1], extent[2], extent[3], width, height, pixelRatio, this.layerIds, ); resolve(createImageBitmap(imageData)); }); }, ...options, }); this.api = api; this.renderFunction = options.renderFunction; this.layerIds = options.layerIds; } } ================================================ FILE: packages/qgis-js-ol/src/QgisJobDataSource.ts ================================================ import type { QgisApi, QgsMapRendererJob } from "qgis-js"; import ImageSource, { Options } from "ol/source/Image"; import ImageWrapper from "ol/Image"; import { getWidth, getHeight, getCenter, containsExtent } from "ol/extent"; import { create as createTransform, compose as composeTransform, } from "ol/transform"; export interface QgisJobDataSourceOptions extends Options { /** * Optional array of layer IDs to render. If omitted, renders all visible layers. */ layerIds?: string[]; /** * Specifies whether to enable preview mode. * (default: true) */ preview?: boolean; /** * Specifies the timeout to wait before rendering the next preview (in milliseconds). * (default: 200) */ previewTimeout?: number; /** * Specifies whether to enable overlay of the last fully rendered image on top of the previews. * (default: true) */ previewOverlay?: boolean; } export class QgisJobDataSource extends ImageSource { protected api: QgisApi; protected layerIds: string[] | undefined; protected preview: boolean; protected previewTimeout: number; protected previewOverlay: boolean; private lastImage: ImageWrapper | null = null; private jobs: QgsMapRendererJob[] = []; constructor(api: QgisApi, options: QgisJobDataSourceOptions = {}) { super({ loader: (extent, resolution, requestPixelRatio) => { return new Promise(async (resolve) => { this.dispatchEvent("jobstart"); this.killPendingJobs(); const pixelRatio = requestPixelRatio || window?.devicePixelRatio || 1; const imageResolution = resolution / pixelRatio; const width = Math.round(getWidth(extent) / imageResolution); const height = Math.round(getHeight(extent) / imageResolution); const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d"); const job = api.renderJob( this.getProjection()?.getCode() || "EPSG:3857", new api.QgsRectangle(extent[0], extent[1], extent[2], extent[3]), width, height, pixelRatio, this.layerIds, ); this.jobs.push(job); const putRenderedImage = () => { const data = new Uint8ClampedArray( job.renderedImage(), ) as Uint8ClampedArray; const imageData = new ImageData(data, width, height); ctx!.putImageData(imageData, 0, 0); }; if (this.preview) { const schedulePreivew = () => { requestAnimationFrame(() => { if (job.isActive()) { // if the preview will be entirely overlaid (e.g. on zooming in), we can skip preview rendering const skipPreviewRendering = this.previewOverlay && this.lastImage && containsExtent(this.lastImage.getExtent(), extent); if (!skipPreviewRendering) { putRenderedImage(); } if (this.previewOverlay && this.lastImage) { const lastImageToDraw = this.lastImage.getImage(); if (lastImageToDraw) { const imageExtent = this.lastImage.getExtent(); const imageResolution = this.lastImage.getResolution(); const [imageResolutionX, imageResolutionY] = Array.isArray(imageResolution) ? imageResolution : [imageResolution, imageResolution]; const imagePixelRatio = this.lastImage.getPixelRatio(); const viewCenter = getCenter(extent); const scaleX = (pixelRatio * imageResolutionX) / (resolution * imagePixelRatio); const scaleY = (pixelRatio * imageResolutionY) / (resolution * imagePixelRatio); const tempTransform = createTransform(); const transform = composeTransform( tempTransform, width / 2, height / 2, scaleX, scaleY, 0, (imagePixelRatio * (imageExtent[0] - viewCenter[0])) / imageResolutionX, (imagePixelRatio * (viewCenter[1] - imageExtent[3])) / imageResolutionY, ); const dw = lastImageToDraw.width * transform[0]; const dh = lastImageToDraw.height * transform[3]; const dx = transform[4]; const dy = transform[5]; ctx!.drawImage( lastImageToDraw, 0, 0, +lastImageToDraw.width, +lastImageToDraw.height, dx, dy, dw, dh, ); } } this.changed(); resolve(canvas); // will have no effect if the canvas is already resolved // schedule the next preview if necessary and the job is still active if (!skipPreviewRendering && job.isActive()) { setTimeout(schedulePreivew, this.previewTimeout); } } }); }; schedulePreivew(); } job.finished(() => { putRenderedImage(); // store the current canvas to reuse it in upcoming previews if (this.preview && this.previewOverlay) { this.lastImage = this.image; } this.changed(); resolve(canvas); // will have no effect if the canvas is already resolved // remove the job from the list of pending jobs this.jobs = this.jobs.filter((j) => j !== job); this.dispatchEvent("jobend"); }); }); }, ...options, }); this.api = api; this.layerIds = options.layerIds; this.preview = typeof options.preview !== "undefined" ? options.preview : true; this.previewTimeout = typeof options.previewTimeout !== "undefined" ? options.previewTimeout : 200; this.previewOverlay = typeof options.previewOverlay !== "undefined" ? options.previewOverlay : true; } public killPendingJobs() { while (this.jobs.length > 0) { const job = this.jobs.pop(); if (job && job.isActive()) { new Promise((resolve) => { job.cancelWithoutBlocking(); resolve(null); }); } } } } ================================================ FILE: packages/qgis-js-ol/src/QgisXYZDataSource.ts ================================================ import type { QgisApi } from "qgis-js"; import XYZ, { Options } from "ol/source/XYZ"; import { createCanvasContext2D } from "ol/dom"; import { toSize } from "ol/size"; import type { TileCoord } from "ol/tilecoord"; import ImageTile from "ol/ImageTile"; export interface QgisXYZDataSourceOptions extends Options { extentBufferFactor?: number | (() => number); layerIds?: string[]; renderFunction?: QgisXYZRenderFunction; debug?: boolean; } export type QgisXYZRenderFunction = ( api: QgisApi, tileCoord: TileCoord, tileSize: number, pixelRatio: number, extentBufferFactor: number, layerIds?: string[], ) => Promise; export class QgisXYZDataSource extends XYZ { protected api: QgisApi; protected static DEFAULT_RENDERFUNCTION: QgisXYZRenderFunction = ( api: QgisApi, tileCoord: TileCoord, tileSize: number, devicePixelRatio: number, extentBufferFactor: number, layerIds?: string[], ) => { return api.renderXYZTile( tileCoord[1], tileCoord[2], tileCoord[0], tileSize, devicePixelRatio, extentBufferFactor, layerIds, ); }; protected renderFunction: QgisXYZRenderFunction | undefined; protected layerIds: string[] | undefined; protected getrenderFunction(): QgisXYZRenderFunction { return this.renderFunction || QgisXYZDataSource.DEFAULT_RENDERFUNCTION; } protected static DEFAULT_EXTENTBUFFERFACTOR = 0; protected extentBufferFactor: number | number | (() => number) | undefined; protected getextentBufferFactor(): number { return typeof this.extentBufferFactor === "function" ? this.extentBufferFactor() : this.extentBufferFactor || QgisXYZDataSource.DEFAULT_EXTENTBUFFERFACTOR; } constructor(api: QgisApi, options: QgisXYZDataSourceOptions = {}) { super({ tileUrlFunction: (tileCoord, pixelRatio) => { const tileSize = ( toSize(this.tileGrid!.getTileSize(tileCoord[0])) as [number, number] )[0]; return `${tileSize * (pixelRatio || 1)}`; }, tileLoadFunction: async (tile, text) => { const renderFunction = this.getrenderFunction(); if (this.tileGrid && renderFunction) { console.assert(tile instanceof ImageTile); const imageTile = tile as ImageTile; const tileSize = parseInt(text); const pixelRatio = Math.round(tileSize / 256); const context = createCanvasContext2D(tileSize, tileSize); const imageData = await renderFunction( this.api, tile.getTileCoord(), tileSize, pixelRatio, this.getextentBufferFactor(), this.layerIds, ); context.putImageData(imageData, 0, 0); if (options.debug) { context.strokeStyle = "grey"; context.strokeRect(0.5, 0.5, tileSize + 0.5, tileSize + 0.5); context.fillStyle = "darkgrey"; context.strokeStyle = "black"; context.textAlign = "center"; context.textBaseline = "middle"; context.font = "20px sans-serif"; context.lineWidth = 2; context.strokeText(text, tileSize / 2, tileSize / 2, tileSize); context.fillText(text, tileSize / 2, tileSize / 2, tileSize); } imageTile.setImage(context.canvas); } }, ...options, }); this.api = api; this.renderFunction = options.renderFunction; this.extentBufferFactor = options.extentBufferFactor; this.layerIds = options.layerIds; } } ================================================ FILE: packages/qgis-js-ol/src/index.ts ================================================ export { QgisCanvasDataSource } from "./QgisCanvasDataSource"; export { QgisXYZDataSource } from "./QgisXYZDataSource"; export { QgisJobDataSource } from "./QgisJobDataSource"; ================================================ FILE: packages/qgis-js-ol/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: packages/qgis-js-ol/vite.config.ts ================================================ import { resolve } from "path"; import { defineConfig } from "vite"; import dts from "vite-plugin-dts"; export default defineConfig({ plugins: [ dts({ rollupTypes: true, entryRoot: "src", }), ], build: { lib: { entry: [resolve(__dirname, "src/index.ts")], name: "qgis-js-ol", formats: ["es"], fileName: "qgis-js-ol", }, rollupOptions: { external: ["qgis-js", "ol", new RegExp("^ol/*")], }, }, }); ================================================ FILE: packages/qgis-js-utils/README.md ================================================ # @qgis-js/utils **Utilities to integrate [qgis-js](https://github.com/qgis/qgis-js) into web applications** [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) [![@qgis-js/utils on npm](https://img.shields.io/npm/v/@qgis-js/utils)](https://www.npmjs.com/package/@qgis-js/utils) > ⚠️🧪 **Work in progress**! Currently this project is in public beta ## Installation ```bash npm install -S @qgis-js/utils ``` ## Usage ### `useProjects` Provides an abstraction to load QGIS projects from various sources. ```js import { qgis } from "qgis-js"; import { useProjects } from "@qgis-js/utils"; const { api, fs } = await qgis(); const { openProject, loadLocalProject, loadGithubProjects, loadRemoteProjects, } = useProjects(fs, (projectPath: string) => { api.loadProject(projectPath); }); ``` The following project sources are supported: #### LocalProject Loads 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) ```js await openProject(await loadLocalProject()); ``` #### GithubProject Loads QGIS projects from a GitHub repository with the [GitHub API](https://docs.github.com/en/rest) #### RemoteProject Fetches QGIS projects from a remote server with the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) - If `loadRemoteProjects` is invoked with a string as, it is assumed to be the URL of a JSON file with the following structure: ```json { "name": "projects", "path": "projects", "type": "Folder", "entries": [ { "name": "village", "path": "projects/village", "type": "Folder", "entries": [ { "name": "project.qgs", "path": "projects/village/project.qgs", "type": "File" }, { "name": "rgb.tif", "path": "projects/village/rgb.tif", "type": "File" } ] } ] } ``` - Otherwise a `Folder` object can also be passed directly to `loadRemoteProjects`, see [FileSystem.ts](./src/fs/FileSystem.ts) ## Versioning This 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). ## License [GNU General Public License v2.0](https://github.com/qgis/qgis-js/blob/main/LICENSE) ================================================ FILE: packages/qgis-js-utils/package.json ================================================ { "name": "@qgis-js/utils", "version": "4.0.0", "description": "Utilities to integrate qgis-js into web applications", "license": "GPL-2.0-or-later", "homepage": "https://qgis.github.io/qgis-js/", "repository": { "type": "git", "url": "https://github.com/qgis/qgis-js", "directory": "packages/qgis-js-utils" }, "bugs": { "url": "https://github.com/qgis/qgis-js/issues" }, "type": "module", "main": "dist/qgis-js-utils.js", "types": "dist/qgis-js-utils.d.ts", "files": [ "dist/**/*" ], "scripts": { "build": "vite build" }, "dependencies": { "@types/emscripten": "1.41.5", "browser-fs-access": "0.38.0", "qgis-js": "workspace:*" }, "devDependencies": { "@types/node": "^22.19.15", "typescript": "5.8.2", "vite": "8.0.0", "vite-plugin-dts": "4.5.4" } } ================================================ FILE: packages/qgis-js-utils/src/fs/FileSystem.ts ================================================ export type FileSystemEntryType = "File" | "Folder"; export interface FileSystemEntry { name: string; path: string; type: FileSystemEntryType; } export interface File extends FileSystemEntry {} export interface Folder extends FileSystemEntry { entries: FolderEntries; } export type FolderEntries = Array; /** * Flattens the "Folder" hierarchy and returns an array of folder paths. * * @param entries - The entries of the root folder. * @param basePath - The base path of the root folder. * @returns An array of folder paths. */ export function flatFolders( entries: Folder["entries"], basePath: string, ): string[] { return entries.reduce((acc, entry) => { if (entry.type === "Folder") { return acc.concat([ basePath + "/" + entry.name, ...flatFolders((entry as Folder).entries, basePath + "/" + entry.name), ]); } else { return acc; } }, []); } /** * Flattens the files in a "Folder" hierarchy and returns an array of file paths. * * @param entries - The entries of the root folder. * @param basePath - The base path of the root folder. * @returns An array of file paths. */ export function flatFiles( entries: Folder["entries"], basePath: string, ): string[] { return entries.reduce((acc, entry) => { if (entry.type === "Folder") { return acc.concat( flatFiles((entry as Folder).entries, basePath + "/" + entry.name), ); } else { return acc.concat(basePath + "/" + entry.name); } }, []); } ================================================ FILE: packages/qgis-js-utils/src/fs/GithubProject.ts ================================================ import type { EmscriptenFS } from "qgis-js"; import { Project, PROJECTS_UPLOAD_DIR } from "./Project"; import { Folder, flatFolders, flatFiles } from "./FileSystem"; export const GIT_DEFAULT_BRANCH = "main"; /** * Fetches the contents of a directory in a GitHub repository via the GitHub REST API. * * Be aware of the GitHub API rate limits: https://docs.github.com/en/rest/overview/rate-limits-for-the-rest-api * * @param owner The owner of the repository. * @param repo The name of the repository. * @param path The path to the directory to fetch. Defaults to the root directory. * @param branch The branch to fetch the directory from. Defaults to the default branch of the repository. * @returns A Promise that resolves to an array of objects representing the files and directories in the specified directory. */ export async function fetchGithubDirectory( owner: string, repo: string, path: string = "/", branch: string = GIT_DEFAULT_BRANCH, ) { const url = `https://api.github.com/repos/${owner}/${repo}/contents${path}?ref=${branch}`; const response = await fetch(url); const content = (await response.json()) as Array<{ type: "file" | "dir"; name: string; path: string; sha: string; }>; return content; } /** * Fetches a list of relative file paths in a GitHub repository tree via the GitHub REST API. * * Be aware of the GitHub API rate limits: https://docs.github.com/en/rest/overview/rate-limits-for-the-rest-api * * @param owner The owner of the repository. * @param repo The name of the repository. * @param sha The SHA of the tree to fetch the files from. * @returns A Promise that resolves to an array of relative file paths in the specified tree. */ export async function fetchGithubTreeFiles( owner: string, repo: string, sha: string, ) { const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/${sha}?recursive=1`; const response = await fetch(url); const content = (await response.json()) as { tree: { type: "tree" | "blob"; path: string; }[]; }; return content.tree .filter((entry) => entry.type === "blob") .map((entry) => entry.path); } /** * Fetches the content of a file from a GitHub repository via the GitHub REST API. * * Be aware of the GitHub API rate limits: https://docs.github.com/en/rest/overview/rate-limits-for-the-rest-api * * @param owner The owner of the repository. * @param repo The name of the repository. * @param path The path to the file. Defaults to the root directory. * @param branch The branch to fetch the file from. Defaults to the default branch of the repository. * @returns A Promise that resolves to the file content as a buffer. * @throws An error if the file content is not found. */ export async function fetchGithubFileContent( owner: string, repo: string, path: string = "/", branch: string = GIT_DEFAULT_BRANCH, ) { const url = `https://api.github.com/repos/${owner}/${repo}/contents${path}?ref=${branch}`; const response = await fetch(url); const file = (await response.json()) as { type: "file" | "dir"; content?: string; download_url?: string; }; if (file.content && file.content !== "") { return Uint8Array.from(atob(file.content), (c) => c.charCodeAt(0)).buffer; } else if (file.download_url && file.download_url !== "") { const response = await fetch(file.download_url); return response.arrayBuffer(); } else { throw new Error("File content of " + path + " not found."); } } /** * Maps an array of file paths to a "Folder" structure. * * @param name - The name of the root folder. * @param path - The path of the root folder. * @param files - An array of relative file paths. * @returns The root folder with the mapped "Folder" structure. */ export function mapFilesToFolder( name: string, path: string, files: string[], ): Folder { const root: File | Folder = { name, path, type: "Folder", entries: [] }; files.sort().forEach((path) => { const parts = path.split("/"); let current = root; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; const entry = current.entries.find((entry) => entry.name === part); if (entry) { current = entry as Folder; } else { const folder: Folder = { name: part, path: current.path + "/" + part, type: "Folder", entries: [], }; current.entries.push(folder); current = folder; } } current.entries.push({ name: parts[parts.length - 1], path, type: "File", }); }); return root; } export class GithubProject extends Project { protected folder: Folder; protected path: string; protected owner: string; protected repo: string; protected branch: string; constructor( FS: EmscriptenFS, projectFolder: Folder, owner: string, repo: string, branch: string = GIT_DEFAULT_BRANCH, ) { super(FS, "Github"); this.folder = projectFolder; this.name = projectFolder.name; this.path = projectFolder.path; this.owner = owner; this.repo = repo; this.branch = branch; } getDirectories(): string[] { return [this.path, ...flatFolders(this.folder.entries, this.path)]; } getFiles(): string[] { return flatFiles(this.folder.entries, this.path); } async uploadProject() { // prepare to download all files in parallel const downloads = Object.fromEntries( this.getFiles().map((file) => { return [ file, new Promise(async (resolve, _reject) => { // note that we don't use fetchGithubFileContent here because of the GitHub API rate limits // instead we use the raw.githubusercontent.com URL directly const url = `https://raw.githubusercontent.com/${this.owner}/${this.repo}/${this.branch}/${file}`; const response = await fetch(url); resolve(response.arrayBuffer()); }), ]; }), ); // wait for all the responses await Promise.all([Object.values(downloads)]); // ensure directories exist this.ensureDirectories(); // write files to the runtime FS for (const file of this.getFiles()) { const data = new Uint8Array(await downloads[file]); this.FS.writeFile(PROJECTS_UPLOAD_DIR + "/" + file, data); } } } ================================================ FILE: packages/qgis-js-utils/src/fs/LocalProject.ts ================================================ import type { EmscriptenFS } from "qgis-js"; import { Project, PROJECTS_UPLOAD_DIR } from "./Project"; import { directoryOpen, FileWithDirectoryAndFileHandle, } from "browser-fs-access"; export type LocalEntries = Array; export { directoryOpen as openLocalDirectory }; export class LocalProject extends Project { protected entries: LocalEntries; constructor(FS: EmscriptenFS, entries: LocalEntries) { super(FS, "Local"); this.entries = entries; const possibleNames = entries .map((e) => e.webkitRelativePath) .filter((p) => p && p.length > 0 && p.includes("/")) .map((p) => p.split("/", 1)[0]); if (possibleNames.length < 1) { throw new Error("Could not determine project name"); } // just use the first possible name as final project name this.name = possibleNames[0]; } getDirectories(): string[] { const subFoldersToCreate = new Set(); subFoldersToCreate.add(this.name); for (const entry of this.entries) { const path = (entry as FileWithDirectoryAndFileHandle).webkitRelativePath; subFoldersToCreate.add(path.substring(0, path.lastIndexOf("/"))); } return Array.from(subFoldersToCreate); } getFiles(): string[] { return this.entries.map((e) => e.webkitRelativePath); } async uploadProject(): Promise { // ensure directories exist this.ensureDirectories(); // write files to the runtime FS for (const file of this.entries) { this.FS.writeFile( PROJECTS_UPLOAD_DIR + "/" + file.webkitRelativePath, new Uint8Array(await file.arrayBuffer()), ); } } } ================================================ FILE: packages/qgis-js-utils/src/fs/Project.ts ================================================ import type { EmscriptenFS } from "qgis-js"; export type ProjectType = "Remote" | "Local" | "Github"; export const PROJECTS_UPLOAD_DIR = "/upload/projects"; export abstract class Project { protected FS; type: ProjectType; name!: string; constructor(FS: EmscriptenFS, type: ProjectType) { this.FS = FS; this.type = type; } abstract getDirectories(): string[]; abstract getFiles(): string[]; abstract uploadProject(): Promise; isValid(): boolean { return this.getFiles().length > 0 && this.getProjectFile() !== undefined; } getProjectFile(): string | undefined { const candidates = this.getFiles().filter( (f) => f.endsWith(".qgs") || f.endsWith(".qgz"), ); if (candidates.length == 1) { return candidates[0]; } else if (candidates.length > 1) { console.warn("Found multiple project file candiates"); return candidates[0]; } else { return undefined; } } getDirectoriesToCreate(): string[] { const directories = new Set(); for (const directory of this.getDirectories()) { let direcotryDirs = directory.split("/"); for (let i = 0; i < direcotryDirs.length; i++) { const dirToCreate = direcotryDirs.slice(0, i + 1).join("/"); directories.add(dirToCreate); } } return Array.from(directories).sort(); } ensureDirectories() { // create directories in the runtime FS (if not already existing) for (const directory of this.getDirectoriesToCreate()) { const dirToCreate = PROJECTS_UPLOAD_DIR + "/" + directory; // @ts-ignore (FS by @types/emscripten is missing the analyzePath method...) const node = this.FS.analyzePath(dirToCreate, false); // @ts-ignore if (!node || !node.exists) { this.FS.mkdir(dirToCreate); } } } isProjectUploaded() { return this.FS.readdir(PROJECTS_UPLOAD_DIR).includes(this.name); } } ================================================ FILE: packages/qgis-js-utils/src/fs/RemoteProject.ts ================================================ import type { EmscriptenFS } from "qgis-js"; import { Project, PROJECTS_UPLOAD_DIR } from "./Project"; import { Folder, flatFolders, flatFiles } from "./FileSystem"; export class RemoteProject extends Project { protected folder: Folder; protected path: string; private baseUrl: URL; constructor(FS: EmscriptenFS, basePath: string, projectFolder: Folder) { super(FS, "Remote"); this.baseUrl = new URL(basePath); this.folder = projectFolder; const bsaeFolder = this.baseUrl.pathname.split("/").pop(); this.name = projectFolder.name; this.path = projectFolder.path.replace(new RegExp(`^${bsaeFolder}\/`), ""); } getDirectories(): string[] { return [this.path, ...flatFolders(this.folder.entries, this.path)]; } getFiles(): string[] { return flatFiles(this.folder.entries, this.path); } async uploadProject() { // download all files in parallel const downloads = Object.fromEntries( this.getFiles().map((file) => { return [ file, new Promise((resolve, _reject) => { const remoteUrl = new URL(`${this.baseUrl.href}/${file}`); fetch(remoteUrl).then((response) => response.arrayBuffer().then((buffer) => { resolve(buffer); }), ); }), ]; }), ); // wait for all responses await Promise.all([Object.values(downloads)]); // ensure directories exist this.ensureDirectories(); // write files to the runtime FS for (const file of this.getFiles()) { const data = new Uint8Array(await downloads[file]); this.FS.writeFile(PROJECTS_UPLOAD_DIR + "/" + file, data); } } } ================================================ FILE: packages/qgis-js-utils/src/fs/index.ts ================================================ import type { EmscriptenFS } from "qgis-js"; import { Folder } from "./FileSystem"; import { PROJECTS_UPLOAD_DIR, Project } from "./Project"; import { LocalEntries, LocalProject, openLocalDirectory } from "./LocalProject"; import { RemoteProject } from "./RemoteProject"; import { GithubProject, fetchGithubDirectory, fetchGithubTreeFiles, mapFilesToFolder, } from "./GithubProject"; export function useProjects( fs: EmscriptenFS, onProjectLoaded: (projectFile: string) => void, ) { // ensure PROJECTS_UPLOAD_DIR (and its parent dirs) exist let projectUploadDirs = PROJECTS_UPLOAD_DIR.split("/"); for (let i = 1; i < projectUploadDirs.length; i++) { fs.mkdir(projectUploadDirs.slice(0, i + 1).join("/")); } const openProject = async (project: Project | Promise) => { if (!project) { return; } else { if (project instanceof Promise) { project = await project; } if (!project.isValid()) { throw new Error(`Project "${project.name}" is not valid`); } if (!project.isProjectUploaded()) { await project.uploadProject(); } loadProject(project); } }; const loadProject = (project: Project) => { const projectFile = PROJECTS_UPLOAD_DIR + "/" + project.getProjectFile(); onProjectLoaded(projectFile); }; const loadLocalProject = () => new Promise(async (resolve, reject) => { try { const entries: LocalEntries = (await openLocalDirectory({ recursive: true, mode: "read", })) as LocalEntries; //TODO: This cast is probably not working when "fs-browser-fs-access" is using the fallback implementation const localProject = new LocalProject(fs, entries); resolve(localProject); } catch (error) { reject(error); } }); const loadRemoteProjects = ( remoteProjects: string = "./projects/directory-listing.json", ) => new Promise(async (resolve, reject) => { try { const url = new URL(remoteProjects as string, window.location.href); const basePath = url.href.split("/").slice(0, -1).join("/"); const remoteProjectsResponse = await fetch(remoteProjects); const remoteProjectsResponseJson = await remoteProjectsResponse.json(); const remoteFolder = remoteProjectsResponseJson as Folder; if (!remoteFolder.type || remoteFolder.type !== "Folder") { reject(new Error("Remote projects response seems not a folder")); return; } resolve( remoteFolder.entries .filter((entry) => entry.type === "Folder") .map((entry) => new RemoteProject(fs, basePath, entry as Folder)), ); } catch (error) { reject(error); } }); const loadGithubProjects = ( owner: string, repo: string, path: string = "/", branch: string = "main", ) => new Promise<{ [key: string]: () => Promise }>( async (resolve, reject) => { try { const projects = await fetchGithubDirectory( owner, repo, path, branch, ); // check if the response got an error message if (!(projects instanceof Array)) { console.warn(projects); resolve({}); return; } else { resolve( Object.fromEntries( projects .filter((entry) => entry.type === "dir") .map((entry) => { return [ entry.name, () => { return new Promise(async (resolve) => { const files = await fetchGithubTreeFiles( owner, repo, entry.sha, ); resolve( new GithubProject( fs, mapFilesToFolder(entry.name, entry.path, files), owner, repo, branch, ), ); }); }, ]; }), ), ); } } catch (error) { reject(error); } }, ); return { openProject, loadLocalProject, loadRemoteProjects, loadGithubProjects, }; } ================================================ FILE: packages/qgis-js-utils/src/index.ts ================================================ export { useProjects } from "./fs"; export type { Project } from "./fs/Project"; export type { FileSystemEntry, File, Folder } from "./fs/FileSystem"; ================================================ FILE: packages/qgis-js-utils/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: packages/qgis-js-utils/vite.config.ts ================================================ import { resolve } from "path"; import { defineConfig } from "vite"; import dts from "vite-plugin-dts"; export default defineConfig({ plugins: [ dts({ rollupTypes: true, entryRoot: "src", }), ], build: { lib: { entry: [resolve(__dirname, "src/index.ts")], name: "qgis-js-utils", formats: ["es"], fileName: "qgis-js-utils", }, rollupOptions: { external: ["qgis-js", "public/projects"], }, }, }); ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - packages/* - sites/* onlyBuiltDependencies: - esbuild ================================================ FILE: qgis-js.ts ================================================ #!/usr/bin/env -S node_modules/.bin/vite-node --script /* * qgis-js CLI * * Will help you to build qgis-js and generate its documentation. * * Run "qgis-js --help" for more information. */ import { CommandLineParser } from "@rushstack/ts-command-line"; import { CleanAction } from "./build/actions/clean"; import { InstallAction } from "./build/actions/install"; import { CompileAction } from "./build/actions/compile"; import { LibsAction } from "./build/actions/libs"; import { SizeAction } from "./build/actions/size"; import { QgisJsOptions } from "./build/actions/lib/QgisJsOptions"; const options: QgisJsOptions = {} as QgisJsOptions; export class QgisJsCommandLine extends CommandLineParser { public constructor() { super({ toolFilename: "qgis-js", toolDescription: 'The "qgis-js" build tool.', }); this.addAction(new CleanAction(options)); this.addAction(new InstallAction(options)); this.addAction(new CompileAction(options)); this.addAction(new LibsAction(options)); this.addAction(new SizeAction(options)); this.defineFlagParameter({ parameterLongName: "--verbose", parameterShortName: "-v", description: "Show extra logging detail", }); } protected onExecuteAsync(): Promise { options.verbose = this.getFlagParameter("--verbose")?.value || false; process.env.FORCE_COLOR = "1"; return super.onExecuteAsync(); } } const qgisjs = new QgisJsCommandLine(); qgisjs.executeAsync(); ================================================ FILE: sites/dev/index.html ================================================ qgis-js

qgis-js

QGIS core ported to WebAssembly to run it on the web platform


Initializing...

OpenLayers: Canvas Tile

qgis-js provides a custom «Canvas Tile» OpenLayers 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.

See QgisCanvasDataSource.ts for the implementation and the olDemoCanvas-function for the demo setup used on this page.

OpenLayers: Preview

 
 

OpenLayers: XYZ Tiles

0.50
 

With the qgis-js custom XYZ Tiles source for OpenLayers, the QGIS project is rendered 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.

See QgisXYZDataSource.ts for the implementation and the olDemoXYZ-function for the demo setup used on this page.

JavaScript

qgis-js can be used with plain JavaScript and can therefore be integrated into any web application. The library provides also type information and integrates nicely with TypeScript.

The example above shows how to use qgis-js with a simple canvas element and some rudimentary controls. The source code can be found in sites/dev/src/js.ts

The qgis-js API exposes also some QGIS core internals which can be used to build web GIS applications:

const rectangle = new api.QgsRectangle(100, 200, 150, 250);
console.dir(rectangle);

rectangle.scale(5);
console.dir(rectangle);

const center = rectangle.center();
console.dir(center);

Please note that the API surface is fairly minimal at the moment and will be extended in the future.

Fork me on GitHub ================================================ FILE: sites/dev/package.json ================================================ { "name": "@qgis-js/dev", "version": "4.0.0", "private": true, "type": "module", "scripts": { "dev": "vite dev", "build": "vite build", "preview": "vite preview", "deploy": "npm run build && npm run deploy:upload", "deploy:upload": "rsync --recursive --delete-before dist/ tux@zrhwpk.asuscomm.com:/data/https-portal/vhosts/qgis-js.dev.schmuki.io" }, "dependencies": { "@qgis-js/ol": "workspace:*", "@qgis-js/utils": "workspace:*", "qgis-js": "workspace:*" }, "devDependencies": { "coi-serviceworker": "0.1.7", "ol": "^10.8.0", "typescript": "5.8.2", "vite": "8.0.0", "vite-plugin-static-copy": "3.3.0" } } ================================================ FILE: sites/dev/public/projects/village/buildings.prj ================================================ 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]] ================================================ FILE: sites/dev/public/projects/village/buildings.qpj ================================================ 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"]] ================================================ FILE: sites/dev/public/projects/village/project.qgs ================================================ 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]] +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +units=m +no_defs 2437 27700 EPSG:27700 OSGB 1936 / British National Grid tmerc EPSG:7001 false rgb_6623eb8c_6d94_452c_b9fa_efb9f472ca24 buildings_c409011b_d787_426a_bb0e_ae5d79d6e4d7 meters 321800.93915886041941121 129496.26018269738415256 321994.0257211935822852 129620.08884272881550714 0 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]] +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +units=m +no_defs 2437 27700 EPSG:27700 OSGB 1936 / British National Grid tmerc EPSG:7001 false 0 Annotations_5a901522_5198_4532_be03_e7edcd668017 Annotations 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]] +proj=longlat +datum=WGS84 +no_defs 3452 4326 EPSG:4326 WGS 84 longlat EPSG:7030 true 0 0 false 1 0 320989.54106238950043917 128983.21402802964439616 322769.35696973575977609 130817.19739320731605403 -3.1290883526975195 51.05473100531580855 -3.10329775497044835 51.07146158335104502 buildings_c409011b_d787_426a_bb0e_ae5d79d6e4d7 ./buildings.shp buildings 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]] +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +units=m +no_defs 2437 27700 EPSG:27700 OSGB 1936 / British National Grid tmerc EPSG:7001 false dataset 0 0 false ogr 1 1 1 0 0 0 1 0 0 generatedlayout 321000 129000.94442700000945479 322800 130800.94442700000945479 -3.12893554519487038 51.05489184983709805 -3.1028644136382888 51.07131958612462341 rgb_6623eb8c_6d94_452c_b9fa_efb9f472ca24 ./rgb.tif rgb 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]] +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +units=m +no_defs 2437 27700 EPSG:27700 OSGB 1936 / British National Grid tmerc EPSG:7001 false 0 0 false gdal 1 1 0 0 MinMax WholeRaster Estimated 0.02 0.98 2 resamplingFilter 0 1 true 0 255 255 255 255 0 255 255 false EPSG:7001 m2 meters 5 2.5 false false 1 0 false false true 0 255,0,0,255 false true 2 MU false 1 None false 1 false conditions unknown 90 1 8 false false 0 false false false 5000 false Martin 2022-03-08T23:01:27 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]] +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +units=m +no_defs 2437 27700 EPSG:27700 OSGB 1936 / British National Grid tmerc EPSG:7001 false ================================================ FILE: sites/dev/src/demo.css ================================================ body { font: 16px/1.5em "Overpass", "Open Sans", Helvetica, sans-serif; color: #333; } h1 { color: #589632; } a { color: #589632; font-weight: bold; } a.source { font-family: "Courier New", Courier, monospace; font-weight: normal; } .demo { position: relative; width: 100%; height: 30em; margin: 2em 0; border: 1px solid #ccc; background-color: #ffffff; resize: both; } @keyframes loading { 0%, 100% { transform: scaleX(0); } 50% { transform: scaleX(1); } } .demo::before, .demo.spinner::before { content: ""; transition: opacity 0.5s ease-in-out; } .demo::before { opacity: 0; } .demo.spinner::before { opacity: 1; display: block; position: absolute; bottom: -0.5em; left: 0.5em; right: 0.5em; height: 1px; background-color: #589632; animation: loading 1.5s ease-in-out infinite; transform-origin: center; } .demo.spinner:fullscreen::before { bottom: 0.25em; } .canvas-options { margin: -1em 0 2em 0; padding: 1em; border: 1px solid #ccc; background-color: #ffffff; text-align: center; } .canvas-options .seperator { display: inline-block; margin: 0 0.5em; border-left: 1px solid #ccc; } .code { overflow: auto; border: 1px solid #ccc; padding: 0.5em 1em; font-size: medium; } #layers-control { position: fixed; bottom: 0; right: 2em; } #layers-control .layers, .themes { width: 25em; padding: 1em 0.5em; border: 1px solid #ccc; border-bottom: none; background-color: #ffffff; } #layers-control .layers .layers-toolbar { float: right; } #layers-control .layers .layers-download { cursor: pointer; user-select: none; font-size: 0.75em; opacity: 0.3; } #layers-control .layers .layers-download:hover { opacity: 0.7; color: #589632; } #layers-control .layers::before, .themes::before { margin: 1em 0.5em; font-weight: bold; content: "Layers:"; } #layers-control .themes::before { content: "Map Theme:"; } #layers-control .themes select { float: right; width: 18em; } #layers-control .layers .layer-tree { margin-top: 0.5em; font-size: 0.85em; } #layers-control .layers .layer-header { display: flex; align-items: center; padding: 2px 0; gap: 4px; white-space: nowrap; } #layers-control .layers .layer-header.disabled { opacity: 0.4; } #layers-control .layers .layer-toggle { cursor: pointer; user-select: none; width: 1em; text-align: center; flex-shrink: 0; } #layers-control .layers .layer-name { overflow: hidden; text-overflow: ellipsis; flex: 1; min-width: 0; } #layers-control .layers .layer-name.is-group { font-weight: 600; } #layers-control .layers .layer-slider { width: 5em; flex-shrink: 0; } #layers-control .layers .layer-filter-icon { cursor: pointer; user-select: none; flex-shrink: 0; font-size: 0.9em; opacity: 0.3; } #layers-control .layers .layer-filter-icon:hover { opacity: 0.7; } #layers-control .layers .layer-filter-icon.active { opacity: 1; color: #589632; } #layers-control .layers .layer-filter { padding-left: 2.2em; padding-bottom: 2px; } #layers-control .layers .layer-filter input { width: 100%; font-size: 0.85em; padding: 2px 4px; border: 1px solid #ccc; border-radius: 2px; } #layers-control .layers .layer-filter input.filter-error { border-color: #c00; background-color: #fee; } #layers-control .layers .layer-legend-icon { cursor: pointer; user-select: none; flex-shrink: 0; font-size: 0.75em; opacity: 0.3; } #layers-control .layers .layer-legend-icon:hover { opacity: 0.7; } #layers-control .layers .layer-legend-icon.active { opacity: 1; color: #589632; } #layers-control .layers .layer-legend { padding-left: 2.2em; margin: 2px 0; } #layers-control .layers .legend-item { display: flex; align-items: center; gap: 4px; padding: 1px 0; font-size: 0.8em; color: #666; } #layers-control .layers .legend-icon { width: 16px; height: 16px; flex-shrink: 0; } #layers-control .layers .legend-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #layers-control .layers .layer-children { padding-left: 1.2em; border-left: 1px solid #ddd; margin-left: 0.45em; } #js-demo-canvas { position: absolute; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; } #js-demo-controls { position: absolute; bottom: 0; left: 0; right: 0; padding: 1em; text-align: center; } .tabset > input[type="radio"] { position: absolute; left: -200vw; } .tabset .tab-panel { display: none; } .tabset > input:first-child:checked ~ .tab-panels > .tab-panel:first-child, .tabset > input:nth-child(3):checked ~ .tab-panels > .tab-panel:nth-child(2), .tabset > input:nth-child(5):checked ~ .tab-panels > .tab-panel:nth-child(3), .tabset > input:nth-child(7):checked ~ .tab-panels > .tab-panel:nth-child(4), .tabset > input:nth-child(9):checked ~ .tab-panels > .tab-panel:nth-child(5), .tabset > input:nth-child(11):checked ~ .tab-panels > .tab-panel:nth-child(6) { display: block; } .tabset > label { position: relative; display: inline-block; padding: 15px 15px 25px; border: 1px solid transparent; border-bottom: 0; cursor: pointer; font-weight: 600; } .tabset > label::after { content: ""; position: absolute; left: 15px; bottom: 10px; width: 22px; height: 4px; background: #8d8d8d; } input:focus-visible + label { outline: 2px solid #589632; border-radius: 3px; } .tabset > label:hover, .tabset > input:focus + label, .tabset > input:checked + label { color: #589632; } .tabset > label:hover::after, .tabset > input:focus + label::after, .tabset > input:checked + label::after { background: #589632; } .tabset > input:checked + label { border-color: #ccc; border-bottom: 1px solid #fff; margin-bottom: -1px; } .tab-panel { padding: 30px 0; border-top: 1px solid #ccc; } *, *:before, *:after { box-sizing: border-box; } body { padding: 30px; } .tabset { position: relative; max-width: 65em; } #project { position: absolute; top: 0; right: 0; padding: 15px 0; } #project label { margin-right: 1em; } #project > div { display: inline; padding-right: 15px; } #projects { min-width: 15em; max-width: 20em; } #status { position: absolute; top: 0; right: 0; padding: 15px 0; } #status div { display: inline-block; } .lds-ellipsis { display: inline-block; position: relative; width: 80px; height: 1em; margin-left: 0.5em; padding-top: 0.25em; } .lds-ellipsis div { position: absolute; width: 13px; height: 13px; border-radius: 50%; background: #589632; animation-timing-function: cubic-bezier(0, 1, 1, 0); } .lds-ellipsis div:nth-child(1) { left: 8px; animation: lds-ellipsis1 0.6s infinite; } .lds-ellipsis div:nth-child(2) { left: 8px; animation: lds-ellipsis2 0.6s infinite; } .lds-ellipsis div:nth-child(3) { left: 32px; animation: lds-ellipsis2 0.6s infinite; } .lds-ellipsis div:nth-child(4) { left: 56px; animation: lds-ellipsis3 0.6s infinite; } @keyframes lds-ellipsis1 { 0% { transform: scale(0); } 100% { transform: scale(1); } } @keyframes lds-ellipsis3 { 0% { transform: scale(1); } 100% { transform: scale(0); } } @keyframes lds-ellipsis2 { 0% { transform: translate(0, 0); } 100% { transform: translate(24px, 0); } } /*! * "Fork me on GitHub" CSS ribbon v0.2.3 | MIT License * https://github.com/simonwhitaker/github-fork-ribbon-css */ .github-fork-ribbon { width: 12.1em; height: 12.1em; position: absolute; overflow: hidden; top: 0; right: 0; z-index: 9999; pointer-events: none; font-size: 13px; text-decoration: none; text-indent: -999999px; } .github-fork-ribbon.fixed { position: fixed; } .github-fork-ribbon:hover, .github-fork-ribbon:active { background-color: rgba(0, 0, 0, 0); } .github-fork-ribbon:before, .github-fork-ribbon:after { /* The right and left classes determine the side we attach our banner to */ position: absolute; display: block; width: 15.38em; height: 1.54em; top: 3.23em; right: -3.23em; -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); } .github-fork-ribbon:before { content: ""; /* Add a bit of padding to give some substance outside the "stitching" */ padding: 0.38em 0; /* Set the base colour */ background-color: #589632; /* Set a gradient: transparent black at the top to almost-transparent black at the bottom */ background-image: -webkit-gradient( linear, left top, left bottom, from(rgba(0, 0, 0, 0)), to(rgba(0, 0, 0, 0.15)) ); background-image: -webkit-linear-gradient( top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15) ); background-image: -moz-linear-gradient( top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15) ); background-image: -ms-linear-gradient( top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15) ); background-image: -o-linear-gradient( top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15) ); background-image: linear-gradient( to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15) ); /* Add a drop shadow */ -webkit-box-shadow: 0 0.15em 0.23em 0 rgba(0, 0, 0, 0.5); -moz-box-shadow: 0 0.15em 0.23em 0 rgba(0, 0, 0, 0.5); box-shadow: 0 0.15em 0.23em 0 rgba(0, 0, 0, 0.5); pointer-events: auto; } .github-fork-ribbon:after { /* Set the text from the data-ribbon attribute */ content: attr(data-ribbon); /* Set the text properties */ color: #fff; font: 700 1em "Helvetica Neue", Helvetica, Arial, sans-serif; line-height: 1.54em; text-decoration: none; text-shadow: 0 -0.08em rgba(0, 0, 0, 0.5); text-align: center; text-indent: 0; /* Set the layout properties */ padding: 0.15em 0; margin: 0.15em 0; /* Add "stitching" effect */ border-width: 0.08em 0; border-style: dotted; border-color: #fff; border-color: rgba(255, 255, 255, 0.7); } .github-fork-ribbon.left-top, .github-fork-ribbon.left-bottom { right: auto; left: 0; } .github-fork-ribbon.left-bottom, .github-fork-ribbon.right-bottom { top: auto; bottom: 0; } .github-fork-ribbon.left-top:before, .github-fork-ribbon.left-top:after, .github-fork-ribbon.left-bottom:before, .github-fork-ribbon.left-bottom:after { right: auto; left: -3.23em; } .github-fork-ribbon.left-bottom:before, .github-fork-ribbon.left-bottom:after, .github-fork-ribbon.right-bottom:before, .github-fork-ribbon.right-bottom:after { top: auto; bottom: 3.23em; } .github-fork-ribbon.left-top:before, .github-fork-ribbon.left-top:after, .github-fork-ribbon.right-bottom:before, .github-fork-ribbon.right-bottom:after { -webkit-transform: rotate(-45deg); -moz-transform: rotate(-45deg); -ms-transform: rotate(-45deg); -o-transform: rotate(-45deg); transform: rotate(-45deg); } .github-fork-ribbon:before { background-color: #589632; } ================================================ FILE: sites/dev/src/index.ts ================================================ import { QGIS_JS_VERSION, qgis } from "qgis-js"; import { QgisApi } from "qgis-js"; import { useProjects } from "@qgis-js/utils"; import type { Project } from "@qgis-js/utils"; import { jsDemo } from "./js"; import { olPreview, olDemoXYZ, olDemoCanvas } from "./ol"; import { layersControl } from "./layers"; const printVersion = true; const apiTest = false; const timer = false; function isDev() { // @ts-ignore return import.meta.env.MODE === "development"; } const qgisJsDemoProjects = (path: string) => ({ owner: "boardend", repo: "qgis-js-projects", path: "/" + path, branch: "main", prefix: `${path[0].toUpperCase()}${path.slice(1)}: `, }); const GITHUB_REPOS: Array<{ owner: string; repo: string; path?: string; branch?: string; prefix?: string; }> = [ qgisJsDemoProjects("demo"), ...(isDev() ? ["test", "performance"].map((path) => qgisJsDemoProjects(path)) : []), ]; function testApi(api: QgisApi) { const p1 = new api.QgsPointXY(); console.dir(p1); const r1 = new api.QgsRectangle(); console.log(r1); const r2 = new api.QgsRectangle(1, 2, 3, 4); console.log(r2.xMinimum, r2.yMinimum, r2.xMaximum, r2.yMaximum); r2.scale(5); console.log(r2.xMinimum, r2.yMinimum, r2.xMaximum, r2.yMaximum); } async function initDemo() { if (printVersion) { console.log(`qgis-js (${QGIS_JS_VERSION})`); } const statusControl = document.getElementById("status")! as HTMLDivElement; const projectControl = document.getElementById("project")! as HTMLDivElement; let isError = false; const onStatus = (status: string) => { if (isError) return; (statusControl.firstElementChild! as HTMLDivElement).innerHTML = status; }; const onError = (error: Error | any) => { isError = true; console.error(error); const message = "" + error && error["message"] ? error["message"] : "Runtime error"; projectControl.style.visibility = "none"; statusControl.style.display = "auto"; statusControl.innerHTML = ``; }; const onReady = () => { statusControl.style.display = "none"; projectControl.style.visibility = "visible"; }; try { // boot the runtime if (timer) console.time("boot"); const { api, fs } = await qgis({ // use assets form QgisRuntimePlugin prefix: new URL("assets/wasm", window.location.href).pathname, onStatus: (status: string) => onStatus(status), }); if (timer) console.timeEnd("boot"); // prepare project management onStatus("Loading projects..."); const updateCallbacks: Array = []; const renderCallbacks: Array = []; const { openProject, loadLocalProject, loadRemoteProjects, loadGithubProjects, } = useProjects(fs, (project: string) => { if (timer) console.time("project"); api.loadProject(project); if (timer) console.timeEnd("project"); // update all demos setTimeout(() => { updateCallbacks.forEach((update) => update()); }, 0); }); const projects = new Map Project | Promise>(); const projectSelect = document.getElementById( "projects", )! as HTMLSelectElement; projectSelect.addEventListener("change", () => { const project = projects.get(projectSelect.value); if (project) { openProject(project()); } }); const listProject = ( name: string, projectLoadFunciton: () => Project | Promise, ) => { projects.set(name, projectLoadFunciton); const option = document.createElement("option"); option.value = name; option.text = name; projectSelect.add(option, null); }; document.getElementById("local-project")!.onclick = async function () { const localProject = await loadLocalProject(); await openProject(localProject); listProject(localProject.name, () => localProject); projectSelect.value = localProject.name; }; // load remote projects if (timer) console.time("remote projects"); // - remote projects const remoteProjects = await loadRemoteProjects(); remoteProjects.forEach((project) => listProject(project.name, () => project), ); if (timer) console.timeEnd("remote projects"); // - github projects if (timer) console.time("github projects"); for (const repo of GITHUB_REPOS) { try { const githubProjects = await loadGithubProjects( repo.owner, repo.repo, repo.path, repo.branch, ); Object.entries(githubProjects).forEach(([name, projectLoadPromise]) => { listProject((repo.prefix || "") + name, projectLoadPromise); }); } catch (error) { console.warn( `Unable to load GitHub project "${repo.owner}/${repo.repo}"`, error, ); } } if (timer) console.timeEnd("github projects"); // open first project onStatus("Opening first project..."); await openProject(remoteProjects[0]); // API tests if (apiTest) testApi(api); // paint a first dummy frame onStatus("Rendering first frame..."); if (timer) console.time("first frame"); await api.renderImage(api.srid(), api.fullExtent(), 42, 42, 1); if (timer) console.timeEnd("first frame"); onReady(); const layersControlDiv = document.getElementById( "layers-control", ) as HTMLDivElement | null; if (layersControlDiv) { updateCallbacks.push( layersControl(layersControlDiv, api, () => { // update all demos setTimeout(() => { renderCallbacks.forEach((render) => render()); }, 0); }), ); } // js demo const jsDemoCanvas = document.getElementById( "js-demo-canvas", ) as HTMLCanvasElement | null; if (jsDemoCanvas) { const { update, render } = jsDemo(jsDemoCanvas, api); updateCallbacks.push(update); renderCallbacks.push(render); // ensure js demo gets refreshed when the section gets visible const jsButton = document.getElementById("tab1") as HTMLInputElement; jsButton.addEventListener("change", () => { if (jsButton.checked) update(); }); } // ol demo const olDemoPreviewDiv = document.getElementById( "ol-demo-preview", ) as HTMLDivElement | null; if (olDemoPreviewDiv) { const { update, render } = olPreview(olDemoPreviewDiv, api); updateCallbacks.push(update); renderCallbacks.push(render); } const olDemoCanvasDiv = document.getElementById( "ol-demo-canvas", ) as HTMLDivElement | null; if (olDemoCanvasDiv) { const { update, render } = olDemoCanvas(olDemoCanvasDiv, api); updateCallbacks.push(update); renderCallbacks.push(render); } const olDemoXYZDiv = document.getElementById( "ol-demo-xyz", ) as HTMLDivElement | null; if (olDemoXYZDiv) { const { update, render } = olDemoXYZ(olDemoXYZDiv, api); updateCallbacks.push(update); renderCallbacks.push(render); } } catch (error) { onError(error); } } document.addEventListener("DOMContentLoaded", function () { initDemo(); }); ================================================ FILE: sites/dev/src/js.ts ================================================ import { QgisApi } from "qgis-js"; import type { QgsRectangle } from "qgis-js"; const mapScaleFactor = 1.5; const mapMoveFactor = 0.1; export function jsDemo( canvas: HTMLCanvasElement, api: QgisApi, ): { update: () => void; render: () => void } { let lastExtent: QgsRectangle | null = null; // ensure pixel perfect rendering // see https://web.dev/articles/device-pixel-content-box const observer = new ResizeObserver((entries) => { const entry = entries.find((entry) => entry.target === canvas); if (entry) { canvas.width = entry.devicePixelContentBoxSize[0].inlineSize; canvas.height = entry.devicePixelContentBoxSize[0].blockSize; } renderMap(); }); observer.observe(canvas, { box: "device-pixel-content-box" }); async function renderMap() { var devicePixelRatio = window.devicePixelRatio || 1; var cssRect = canvas.getBoundingClientRect(); const imageWidth = cssRect.width * devicePixelRatio; const imageHeight = cssRect.height * devicePixelRatio; if (imageWidth && imageHeight) { const image = await api.renderImage( api.srid(), lastExtent!, imageWidth, imageHeight, window.devicePixelRatio, ); const context = canvas.getContext("2d"); canvas.width = imageWidth; canvas.height = imageHeight; context!.scale(devicePixelRatio, devicePixelRatio); context!.putImageData(image, 0, 0); } } document.getElementById("zoomin")!.onclick = function () { lastExtent!.scale(1 / mapScaleFactor); renderMap(); }; document.getElementById("zoomout")!.onclick = function () { lastExtent!.scale(mapScaleFactor); renderMap(); }; document.getElementById("panleft")!.onclick = function () { lastExtent!.move( (lastExtent!.xMaximum - lastExtent!.xMinimum) * mapMoveFactor, 0, ); renderMap(); }; document.getElementById("panright")!.onclick = function () { lastExtent!.move( -(lastExtent!.xMaximum - lastExtent!.xMinimum) * mapMoveFactor, 0, ); renderMap(); }; document.getElementById("panup")!.onclick = function () { lastExtent!.move( 0, -(lastExtent!.yMaximum - lastExtent!.yMinimum) * mapMoveFactor, ); renderMap(); }; document.getElementById("pandown")!.onclick = function () { lastExtent!.move( 0, (lastExtent!.yMaximum - lastExtent!.yMinimum) * mapMoveFactor, ); renderMap(); }; function onStart() { lastExtent = api.fullExtent(); renderMap(); } onStart(); return { update: () => { onStart(); }, render: () => { renderMap(); }, }; } ================================================ FILE: sites/dev/src/layers.ts ================================================ import type { QgisApi, QgsLayerTreeNode, QgsLayerTreeLayer, QgsVectorLayer, } from "qgis-js"; import { LayerType } from "qgis-js"; function renderNode( parent: HTMLElement, node: QgsLayerTreeNode, redraw: () => void, update: () => void, parentVisible: boolean = true, ) { const el = document.createElement("div"); const header = document.createElement("div"); header.className = "layer-header"; const visible = parentVisible && node.itemVisibilityChecked; if (!visible) header.classList.add("disabled"); if (node.isGroup()) { const toggle = document.createElement("span"); toggle.className = "layer-toggle"; toggle.textContent = node.expanded ? "\u25BE" : "\u25B8"; toggle.addEventListener("click", () => { node.expanded = !node.expanded; update(); }); header.appendChild(toggle); } else { const spacer = document.createElement("span"); spacer.className = "layer-toggle"; header.appendChild(spacer); } const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = node.itemVisibilityChecked; checkbox.addEventListener("change", () => { node.itemVisibilityChecked = checkbox.checked; redraw(); update(); }); header.appendChild(checkbox); const name = document.createElement("span"); name.className = "layer-name"; if (node.isGroup()) name.classList.add("is-group"); name.textContent = node.name; header.appendChild(name); let filterRow: HTMLDivElement | undefined; let legendContainer: HTMLDivElement | undefined; if (node.isLayer()) { const treeLayer = node as QgsLayerTreeLayer; const mapLayer = treeLayer.layer(); if (mapLayer && mapLayer.type() === LayerType.Vector) { const vectorLayer = mapLayer as QgsVectorLayer; const hasFilter = vectorLayer.subsetString() !== ""; const filterIcon = document.createElement("span"); filterIcon.className = "layer-filter-icon"; if (hasFilter) filterIcon.classList.add("active"); filterIcon.title = "SQL filter"; filterIcon.textContent = "\u2AF6"; header.appendChild(filterIcon); filterRow = document.createElement("div"); filterRow.className = "layer-filter"; filterRow.style.display = "none"; const filterInput = document.createElement("input"); filterInput.type = "text"; filterInput.placeholder = "SQL filter expression"; filterInput.value = vectorLayer.subsetString(); const applyFilter = () => { const success = vectorLayer.setSubsetString(filterInput.value); filterInput.classList.toggle("filter-error", !success); filterIcon.classList.toggle("active", filterInput.value !== ""); if (success) redraw(); }; filterInput.addEventListener("change", applyFilter); filterRow.appendChild(filterInput); filterIcon.addEventListener("click", () => { const isVisible = filterRow!.style.display !== "none"; filterRow!.style.display = isVisible ? "none" : ""; if (!isVisible) filterInput.focus(); }); } const legendIcon = document.createElement("span"); legendIcon.className = "layer-legend-icon"; legendIcon.title = "Legend"; legendIcon.textContent = "\u2630"; header.appendChild(legendIcon); legendContainer = document.createElement("div"); legendContainer.className = "layer-legend"; legendContainer.style.display = "none"; legendIcon.addEventListener("click", () => { const isVisible = legendContainer!.style.display !== "none"; legendContainer!.style.display = isVisible ? "none" : ""; legendIcon.classList.toggle("active", !isVisible); if (!isVisible && legendContainer!.childElementCount === 0) { for (const legendNode of treeLayer.legendNodes()) { const row = document.createElement("div"); row.className = "legend-item"; const icon = legendNode.symbolImage(16); if (icon) { const img = document.createElement("img"); img.className = "legend-icon"; img.src = icon; row.appendChild(img); } const label = document.createElement("span"); label.className = "legend-label"; label.textContent = legendNode.label(); row.appendChild(label); legendContainer!.appendChild(row); } } }); if (mapLayer) { const slider = document.createElement("input"); slider.className = "layer-slider"; slider.type = "range"; slider.min = "0"; slider.max = "100"; slider.value = "" + mapLayer.opacity * 100; slider.step = "1"; slider.addEventListener("change", () => { mapLayer.opacity = parseInt(slider.value) / 100; redraw(); }); header.appendChild(slider); } } el.appendChild(header); if (filterRow) el.appendChild(filterRow); if (legendContainer) el.appendChild(legendContainer); if (node.isGroup()) { const childContainer = document.createElement("div"); childContainer.className = "layer-children"; if (!node.expanded) childContainer.style.display = "none"; const children = node.children(); for (const child of children) { renderNode(childContainer, child, redraw, update, visible); } el.appendChild(childContainer); } parent.appendChild(el); } export function layersControl( target: HTMLDivElement, api: QgisApi, redraw: () => void = () => {}, ): () => void { const update = () => { target.innerHTML = ""; const layerContainer = document.createElement("div"); layerContainer.className = "layers"; target.appendChild(layerContainer); const toolbar = document.createElement("div"); toolbar.className = "layers-toolbar"; const downloadIcon = document.createElement("span"); downloadIcon.className = "layers-download"; downloadIcon.title = "Download legend as PNG"; downloadIcon.textContent = "\u2630"; downloadIcon.addEventListener("click", () => { const dataUrl = api.renderLegend(300); if (!dataUrl) return; const link = document.createElement("a"); link.href = dataUrl; link.download = "legend.png"; link.style.display = "none"; document.body.appendChild(link); link.click(); setTimeout(() => document.body.removeChild(link), 100); }); toolbar.appendChild(downloadIcon); layerContainer.appendChild(toolbar); const tree = document.createElement("div"); tree.className = "layer-tree"; layerContainer.appendChild(tree); const root = api.layerTreeRoot(); for (const child of root.children()) { renderNode(tree, child, redraw, update); } if (api.mapThemes().length > 0) { const themeContainer = document.createElement("div"); themeContainer.className = "themes"; target.appendChild(themeContainer); const select = document.createElement("select"); select.addEventListener("change", () => { if (select.value) { api.setMapTheme(select.value); redraw(); update(); } }); themeContainer.appendChild(select); const currentTheme = api.getMapTheme(); const option = document.createElement("option"); option.value = ""; option.text = ""; if (!currentTheme) { option.selected = true; } select.appendChild(option); for (const theme of api.mapThemes()) { const option = document.createElement("option"); option.value = theme; option.text = theme; if (theme === currentTheme) { option.selected = true; } select.appendChild(option); } } }; update(); return update; } ================================================ FILE: sites/dev/src/ol.ts ================================================ import { QgisApi } from "qgis-js"; import { QgisJobDataSource, QgisCanvasDataSource, QgisXYZDataSource, } from "@qgis-js/ol"; import Map from "ol/Map.js"; import View from "ol/View.js"; import WebGLTileLayer from "ol/layer/WebGLTile.js"; import ImageLayer from "ol/layer/Image"; import XYZ from "ol/source/XYZ.js"; import Projection from "ol/proj/Projection.js"; import { ScaleLine, FullScreen, defaults as defaultControls, } from "ol/control.js"; // @ts-ignore import("ol/ol.css"); const animationDuration = 500; const useBaseMap = true; export function olDemoXYZ( target: HTMLDivElement, api: QgisApi, ): { init: () => void; update: () => void; render: () => void } { let view: View | undefined = undefined; let map: Map | undefined = undefined; let layer: WebGLTileLayer | undefined = undefined; let source: QgisXYZDataSource | undefined = undefined; const getBbox = () => { const initioalSrid = api.srid(); const initialExtent = api.fullExtent(); return initioalSrid === "EPSG:3857" ? initialExtent : api.transformRectangle(initialExtent, initioalSrid, "EPSG:3857"); }; const init = () => { target.innerHTML = ""; const center = getBbox().center(); view = new View({ center: [center.x, center.y], zoom: 10, }); source = new QgisXYZDataSource(api, { debug: false, extentBufferFactor: () => { const input = document.getElementById( "extentBufferFactor", ) as HTMLInputElement; return input.valueAsNumber; }, }); ((layer = new WebGLTileLayer({ source, })), (map = new Map({ target, view, controls: defaultControls().extend([new ScaleLine(), new FullScreen()]), layers: [ layer, new WebGLTileLayer({ visible: useBaseMap, source: new XYZ({ url: `https://tile.openstreetmap.org/{z}/{x}/{y}.png`, }), style: { saturation: ["var", "saturation"], variables: { saturation: -0.75, }, }, }), ].reverse(), }))); map.on("loadstart", function () { map!.getTargetElement().classList.add("spinner"); }); map.on("loadend", function () { map!.getTargetElement().classList.remove("spinner"); }); map.once("precompose", function (_event) { // fit the view to the extent of the data once the map gets actually rendered update(); }); }; const update = () => { const bbox = getBbox(); view!.fit([bbox.xMinimum, bbox.yMinimum, bbox.xMaximum, bbox.yMaximum], { duration: animationDuration, }); setTimeout(() => { render(); }, 0); }; const render = () => { source?.clear(); layer?.getRenderer()?.clearCache(); layer?.changed(); }; const xyzBaseMapCheckbox = document.getElementById( "xyzBaseMap", ) as HTMLInputElement | null; if (xyzBaseMapCheckbox) { xyzBaseMapCheckbox.addEventListener("change", () => { if (map) map.getLayers().getArray()[0].setVisible(xyzBaseMapCheckbox.checked); }); } init(); return { init, update, render, }; } export function olDemoCanvas( target: HTMLDivElement, api: QgisApi, ): { init: () => void; update: () => void; render: () => void } { let view: View | undefined = undefined; let srid: string | undefined = undefined; let map: Map | undefined = undefined; let layer: ImageLayer | undefined = undefined; let source: QgisCanvasDataSource | undefined = undefined; const init = () => { target.innerHTML = ""; srid = api.srid(); const projection = new Projection({ code: srid, // TODO map unit of QgsCoordinateReferenceSystem to ol unit // https://api.qgis.org/api/classQgsCoordinateReferenceSystem.html#ad57c8a9222c27173c7234ca270306128 // https://openlayers.org/en/latest/apidoc/module-ol_proj_Units.html units: "m", }); const bbox = api.fullExtent(); const center = bbox.center(); view = new View({ projection, center: [center.x, center.y], zoom: 10, }); source = new QgisCanvasDataSource(api, { projection, }); layer = new ImageLayer({ source, }); map = new Map({ target, view, controls: defaultControls().extend([new ScaleLine(), new FullScreen()]), layers: [layer], }); map.on("loadstart", function () { map!.getTargetElement().classList.add("spinner"); }); map.on("loadend", function () { map!.getTargetElement().classList.remove("spinner"); }); map.once("precompose", function (_event) { const bbox = api.fullExtent(); view!.fit([bbox.xMinimum, bbox.yMinimum, bbox.xMaximum, bbox.yMaximum], { duration: animationDuration, }); }); }; // recreate the entire map on each update to get new projections working const update = () => { init(); }; const render = () => { setTimeout(() => { // recreate the source to force reload the image in the layer source = new QgisCanvasDataSource(api, { projection: new Projection({ code: srid!, units: "m", }), }); layer?.setSource(source); }, 0); }; init(); return { init, update, render, }; } export function olPreview( target: HTMLDivElement, api: QgisApi, ): { init: () => void; update: () => void; render: () => void } { let view: View | undefined = undefined; let srid: string | undefined = undefined; let map: Map | undefined = undefined; let layer: ImageLayer | undefined = undefined; let source: QgisJobDataSource | undefined = undefined; const inputPreview = document.getElementById( "previewRendering", ) as HTMLInputElement; const inputTimeout = document.getElementById( "previewTimeout", ) as HTMLInputElement; const inputOverlay = document.getElementById( "previewOverlay", ) as HTMLInputElement; const init = () => { target.innerHTML = ""; srid = api.srid(); const projection = new Projection({ code: srid, // TODO map unit of QgsCoordinateReferenceSystem to ol unit // https://api.qgis.org/api/classQgsCoordinateReferenceSystem.html#ad57c8a9222c27173c7234ca270306128 // https://openlayers.org/en/latest/apidoc/module-ol_proj_Units.html units: "m", }); const bbox = api.fullExtent(); const center = bbox.center(); view = new View({ projection, center: [center.x, center.y], zoom: 10, }); source = new QgisJobDataSource(api, { preview: inputPreview?.checked, previewTimeout: inputTimeout?.valueAsNumber, previewOverlay: inputOverlay?.checked, projection, }); layer = new ImageLayer({ source, }); map = new Map({ target, view, controls: defaultControls().extend([new ScaleLine(), new FullScreen()]), layers: [layer], }); // @ts-ignore source.on("jobstart", function () { map!.getTargetElement().classList.add("spinner"); }); // @ts-ignore source.on("jobend", function () { map!.getTargetElement().classList.remove("spinner"); }); map.once("precompose", function (_event) { const bbox = api.fullExtent(); view!.fit([bbox.xMinimum, bbox.yMinimum, bbox.xMaximum, bbox.yMaximum], { duration: animationDuration, }); }); }; // recreate the entire map on each update to get new projections working const update = () => { if (source) { source.killPendingJobs(); } init(); }; inputPreview?.addEventListener("change", () => update()); inputTimeout?.addEventListener("change", () => update()); inputOverlay?.addEventListener("change", () => update()); const render = () => { setTimeout(() => { // recreate the source to force reload the image in the layer source = new QgisJobDataSource(api, { preview: inputPreview?.checked, previewTimeout: inputTimeout?.valueAsNumber, previewOverlay: inputOverlay?.checked, projection: new Projection({ code: srid!, units: "m", }), }); layer?.setSource(source); // @ts-ignore source.on("jobstart", function () { map!.getTargetElement().classList.add("spinner"); }); // @ts-ignore source.on("jobend", function () { map!.getTargetElement().classList.remove("spinner"); }); }, 0); }; init(); return { init, update, render, }; } ================================================ FILE: sites/dev/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: sites/dev/vite.config.ts ================================================ import { resolve } from "path"; import { defineConfig } from "vite"; import QgisRuntimePlugin from "../../build/vite/QgisRuntimePlugin"; import DirectoryListingPlugin from "../../build/vite/DirectoryListingPlugin"; import CrossOriginIsolationPlugin, { CrossOriginIsolationResponseHeaders, } from "../../build/vite/CrossOriginIsolationPlugin"; import { viteStaticCopy } from "vite-plugin-static-copy"; import packageJson from "./package.json"; export default defineConfig({ base: "/qgis-js/", define: { __QGIS_JS_VERSION: JSON.stringify(packageJson.version), }, resolve: { alias: [ // don't use the bundlet version of qgis-js and qgis-js-ol to enable HMR { find: /^qgis-js$/, replacement: resolve(__dirname, "../../packages/qgis-js/src/index.ts"), }, { find: /^@qgis-js\/ol$/, replacement: resolve( __dirname, "../../packages/qgis-js-ol/src/index.ts", ), }, { find: /^@qgis-js\/utils$/, replacement: resolve( __dirname, "../../packages/qgis-js-utils/src/index.ts", ), }, ], }, preview: { headers: { ...CrossOriginIsolationResponseHeaders, }, }, plugins: [ QgisRuntimePlugin({ name: "qgis-js", outputDir: "build/wasm", }), CrossOriginIsolationPlugin(), DirectoryListingPlugin(["public/projects"]), viteStaticCopy({ targets: [ { src: "node_modules/coi-serviceworker/coi-serviceworker.min.js", dest: "", }, ], }), ], }); ================================================ FILE: sites/performance/.gitignore ================================================ /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ /test-results/ /playwright-report/ /blob-report/ /playwright/.cache/ public/projects ================================================ FILE: sites/performance/index.html ================================================ qgis-js-performance

qgis-js Performance Measurement Tool

================================================ FILE: sites/performance/package.json ================================================ { "name": "@qgis-js/performance", "version": "4.0.0", "private": true, "type": "module", "scripts": { "dev": "vite dev", "build": "vite build", "preview": "vite preview", "perf": "playwright test" }, "dependencies": { "@qgis-js/utils": "workspace:*", "qgis-js": "workspace:*" }, "devDependencies": { "@playwright/test": "^1.58.2", "@types/node": "^22.19.15", "typescript": "5.8.2", "vite": "8.0.0", "vite-plugin-static-copy": "3.3.0" } } ================================================ FILE: sites/performance/playwright.config.ts ================================================ import { defineConfig, devices } from "@playwright/test"; export default defineConfig({ testDir: "./tests", retries: 0, fullyParallel: false, workers: 1, reporter: [["list"], ["json", { outputFile: "test-results.json" }], ["html"]], use: { baseURL: "http://127.0.0.1:3000", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", }, projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] }, }, { name: "firefox", use: { ...devices["Desktop Firefox"] }, }, ], webServer: { command: "npm run dev", url: "http://127.0.0.1:3000", stdout: "pipe", stderr: "pipe", }, }); ================================================ FILE: sites/performance/report.html ================================================ Performance Report

Performance Report

Task Project Baseline
Load project
Load project
Load project
================================================ FILE: sites/performance/src/index.ts ================================================ import { qgis, QGIS_JS_VERSION } from "qgis-js"; import type { QgisApi, EmscriptenFS } from "qgis-js"; import { useProjects } from "@qgis-js/utils"; import type { Project } from "@qgis-js/utils"; let API: QgisApi; let FS: EmscriptenFS; const qgisJsDemoProjects = (path: string) => ({ owner: "boardend", repo: "qgis-js-projects", path: "/" + path, branch: "main", prefix: `${path[0].toUpperCase()}${path.slice(1)}: `, }); const GITHUB_REPOS: Array<{ owner: string; repo: string; path?: string; branch?: string; prefix?: string; }> = [qgisJsDemoProjects("performance")]; const PROJECTS = new Map Project | Promise>(); let OPEN_PROJECT: any; async function init() { // add option to boot input select const bootInput = document.querySelector("#boot-input") as HTMLSelectElement; // switching the Runtime is not supported yet bootInput.disabled = true; const option = document.createElement("option"); option.value = "bundle"; option.textContent = `Bundle (${QGIS_JS_VERSION})`; bootInput.appendChild(option); const response = await fetch( "https://api.github.com/repos/qgis/qgis-js/releases", ); const json = await response.json(); for (const release of json) { const option = document.createElement("option"); option.value = release.tag_name; option.textContent = release.tag_name; bootInput.appendChild(option); } const btnBoot = document.querySelector("#boot"); if (btnBoot) { btnBoot.removeAttribute("disabled"); } } async function bootRuntime(_eClick: Event) { let qgisLoader; const runtime: string = ( document.querySelector("#boot-input") as HTMLInputElement ).value; if (runtime === "bundle") { qgisLoader = qgis; } else { //TODO respect runtime selection and load npm packages from CDN /* //see https://github.com/emscripten-core/emscripten/issues/8338 const { qgis } = await import( "https://cdn.jsdelivr.net/npm/qgis-js@0.0.1/dist/qgis.js" ); */ qgisLoader = qgis; } measureStart("boot"); const { api, fs } = await qgisLoader({ prefix: "/assets/wasm", }); measureEnd("boot"); API = api; FS = fs; const btnTest = document.querySelector("#project"); if (btnTest) { btnTest.removeAttribute("disabled"); } const { openProject, loadRemoteProjects, loadGithubProjects } = useProjects( FS, (project: string) => { measureStart("project"); API.loadProject(project); measureEnd("project"); const btnFrame = document.querySelector("#frame"); if (btnFrame) { btnFrame.removeAttribute("disabled"); } }, ); OPEN_PROJECT = openProject; const remoteProjects = await loadRemoteProjects(); remoteProjects.forEach((project) => PROJECTS.set(project.name, () => project), ); for (const repo of GITHUB_REPOS) { try { const githubProjects = await loadGithubProjects( repo.owner, repo.repo, repo.path, repo.branch, ); Object.entries(githubProjects).forEach(([name, projectLoadPromise]) => { PROJECTS.set(name, projectLoadPromise); }); } catch (error) { console.warn( `Unable to load GitHub project "${repo.owner}/${repo.repo}"`, error, ); } } const projectInput = document.querySelector( "#project-input", ) as HTMLInputElement; for (const project of PROJECTS.keys()) { const option = document.createElement("option"); option.value = project; option.textContent = project; projectInput.appendChild(option); } projectInput.disabled = false; } async function loadProject(_eClick: Event) { const project = (document.querySelector("#project-input") as HTMLInputElement) .value; if (!project) { throw new Error("Project not found"); } const selectedProject = PROJECTS.get(project)!; OPEN_PROJECT(selectedProject()); } async function renderFirstFrame(_eClick: Event) { const [width, height] = JSON.parse( (document.querySelector("#frame-input") as HTMLInputElement).value, ); if (!width || !height) { throw new Error("Could not determine width and height"); } measureStart("frame"); await API.renderImage(API.srid(), API.fullExtent(), width, height, 1); measureEnd("frame"); const btnStart = document.querySelector("#start"); if (btnStart) { btnStart.removeAttribute("disabled"); } } async function startPerformanceTest(_eClick: Event) { const loader = document.createElement("div"); loader.id = "loading"; ( _eClick.target as HTMLElement ).parentElement?.parentElement?.firstElementChild?.appendChild(loader); const optionShowResults = document.querySelector( "#option-show-results", ) as HTMLInputElement; const extents = JSON.parse( (document.querySelector("#start-extent-input") as HTMLInputElement).value, ); if (!extents || !Array.isArray(extents) || extents.length === 0) { throw new Error("Invalid extent"); } const resolutions = JSON.parse( (document.querySelector("#start-resolution-input") as HTMLInputElement) .value, ); if (!resolutions || !Array.isArray(resolutions) || resolutions.length === 0) { throw new Error("Invalid resolution"); } const amount = ( document.querySelector("#start-amount-input") as HTMLInputElement ).valueAsNumber; const results = document.querySelector("#results"); const table = document.createElement("table"); // header const row = document.createElement("tr"); const cell = document.createElement("td"); row.appendChild(cell); for (const resolution of resolutions) { const cell = document.createElement("td"); const [width, height] = resolution; cell.textContent = `${width}x${height}`; row.appendChild(cell); } table.appendChild(row); for (const extnet of extents) { const row = document.createElement("tr"); const cell = document.createElement("td"); const title = document.createElement("strong"); title.textContent = extnet.name; cell.appendChild(title); cell.appendChild(document.createTextNode("\u00A0")); const srid = document.createElement("span"); srid.textContent = ` (${extnet.srid})`; cell.appendChild(srid); const br = document.createElement("br"); cell.appendChild(br); const bbox = document.createElement("span"); bbox.textContent = extnet.extent[0].join("/") + ", " + extnet.extent[1].join("/"); cell.appendChild(bbox); row.appendChild(cell); for (const resolution of resolutions) { const cell = document.createElement("td"); cell.classList.add("measure"); for (let i = 0; i < amount; i++) { const input = document.createElement("input"); input.type = "text"; input.id = `measure-${extnet.name}-${resolution[0]}x${resolution[1]}-${i}`; input.dataset.extent = JSON.stringify(extnet.extent); input.dataset.resolution = JSON.stringify(resolution); input.dataset.amount = i.toString(); input.disabled = true; cell.appendChild(input); cell.appendChild(document.createElement("br")); } row.appendChild(cell); } table.appendChild(row); } results?.appendChild(table); const todoSelector = "#results td.measure > input:not([value])"; while (document.querySelector(todoSelector)) { // find all open inputs const potentialJobs = Array.from(document.querySelectorAll(todoSelector)); const randomJob = potentialJobs[ Math.floor(Math.random() * potentialJobs.length) ] as HTMLInputElement; // get first input of this measure with input:not([value]) const job = randomJob.parentElement?.querySelector( "input:not([value])", ) as HTMLInputElement; job.classList.add("active"); const extent = JSON.parse(job.dataset.extent!); const resolution = JSON.parse(job.dataset.resolution!); measureStart(job.id); const result = await API.renderImage( API.srid(), new API.QgsRectangle( extent[0][0], extent[0][1], extent[1][0], extent[1][1], ), resolution[0], resolution[1], 1, ); const time = measureEnd(job.id); job.setAttribute("value", "" + time); if (optionShowResults.checked) { // render the img to the DOM const canvas = new OffscreenCanvas(result.width, result.height); const context = canvas.getContext("2d"); context!.putImageData(result, 0, 0); const img = document.createElement("img"); img.src = URL.createObjectURL(await canvas.convertToBlob()); job.insertAdjacentElement("afterend", img); img.addEventListener("click", () => { // open image in a new tab const newTab = window.open(); newTab?.document.write(``); }); } job.classList.remove("active"); } } document.addEventListener("DOMContentLoaded", (_e) => { document .querySelector("#boot") ?.addEventListener("click", tryCatch(bootRuntime)); document .querySelector("#project") ?.addEventListener("click", tryCatch(loadProject)); document .querySelector("#frame") ?.addEventListener("click", tryCatch(renderFirstFrame)); document .querySelector("#start") ?.addEventListener("click", tryCatch(startPerformanceTest)); init(); }); // Utility functions function measureStart(name: string) { performance.mark(`${name}-start`); } function measureEnd(name: string) { performance.mark(`${name}-end`); const measurement = performance.measure( `${name}-measure`, `${name}-start`, `${name}-end`, ); const element = document.querySelector(`#${name}-measure`); if (element) { (element as HTMLInputElement).valueAsNumber = measurement.duration; } return measurement.duration; } function tryCatch(handler: (event: Event) => void) { return async (event: Event) => { try { await handler(event); } catch (error: any) { reportError(error); throw error; } }; } function reportError(error: any) { document.body.style.backgroundColor = "#ffcccc"; const errorDiv = document.createElement("div"); errorDiv.style.marginTop = "2em"; errorDiv.style.color = "red"; errorDiv.textContent = error.message; document.body.appendChild(errorDiv); } ================================================ FILE: sites/performance/tests/performance.spec.ts ================================================ import { test, expect } from "@playwright/test"; import { join, dirname } from "path"; import { readdirSync } from "fs"; import { fileURLToPath } from "url"; import { createRequire } from "module"; const require = createRequire(import.meta.url); const __dirname = dirname(fileURLToPath(import.meta.url)); test.describe.serial("Performance measurement", () => { // reading test projects const projectsDir = join(__dirname, "..", "public", "projects"); const projectNames = readdirSync(projectsDir); // create a test for each project for (const projectName of projectNames) { console.log(projectName); const json = require(`../public/projects/${projectName}/measure.json`); console.dir(json); test(`Test project "${projectName}"`, async ({ page }) => { await page.goto("/"); await expect(page).toHaveTitle(/^qgis-js-performance$/); const btnBoot = page.locator("#boot"); const btnProject = page.locator("#project"); const btnFrame = page.locator("#frame"); const btnStart = page.locator("#start"); await test.step("Boot runtime", async () => { expect(btnBoot).not.toBeNull(); await btnBoot?.click(); await expect(btnProject).toBeEnabled(); }); await test.step("Load project", async () => { expect(btnProject).not.toBeNull(); const inpProject = page.locator("#project-input"); expect(btnProject).not.toBeNull(); await inpProject.fill(projectName); await btnProject?.click(); await expect(btnFrame).toBeEnabled({ timeout: 10000 }); }); await test.step("Render first frame", async () => { expect(btnFrame).not.toBeNull(); await btnFrame?.click(); await expect(btnStart).toBeEnabled({ timeout: 10000 }); }); await test.step("Run performance measurement", async () => { expect(btnStart).not.toBeNull(); const inpExtent = page.locator("#start-extent-input"); expect(btnProject).not.toBeNull(); await inpExtent.fill(JSON.stringify(json.extent, undefined, "")); const inpResolution = page.locator("#start-resolution-input"); expect(inpResolution).not.toBeNull(); await inpResolution.fill( JSON.stringify(json.resolution, undefined, ""), ); await btnStart?.click(); await page.waitForSelector("#results"); // wait until div with id "loader" is gone await page.waitForSelector("#loading", { state: "detached" }); }); console.log("test done"); }); } }); ================================================ FILE: sites/performance/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "include": ["src"] } ================================================ FILE: sites/performance/vite.config.ts ================================================ import { defineConfig } from "vite"; import QgisRuntimePlugin from "../../build/vite/QgisRuntimePlugin"; import DirectoryListingPlugin from "../../build/vite/DirectoryListingPlugin"; import CrossOriginIsolationPlugin, { CrossOriginIsolationResponseHeaders, } from "../../build/vite/CrossOriginIsolationPlugin"; import packageJson from "./package.json"; export default defineConfig({ define: { __QGIS_JS_VERSION: JSON.stringify(packageJson.version), }, server: { port: 3000, }, preview: { headers: { ...CrossOriginIsolationResponseHeaders, }, }, plugins: [ QgisRuntimePlugin({ name: "qgis-js", outputDir: "build/wasm", }), CrossOriginIsolationPlugin(), DirectoryListingPlugin(["public/projects"]), ], }); ================================================ FILE: src/api/QgisApi.cpp ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../model/QgsLayerTreeLayer.hpp" #include "../model/QgsMapRendererJob.hpp" #include "../model/QgsMapRendererParallelJob.hpp" #include "../model/QgsMapRendererQImageJob.hpp" #include "../model/QgsPointXY.hpp" #include "../model/QgsRectangle.hpp" #include #include QList QgisApi_allLayers() { return QgsProject::instance()->layerTreeRoot()->layerOrder(); } QList QgisApi_visibleLayers() { QList result = {}; auto root = QgsProject::instance()->layerTreeRoot(); const QList allLayers = root->layerOrder(); for (QgsMapLayer *layer : allLayers) { QgsLayerTreeLayer *nodeLayer = root->findLayer(layer->id()); if (nodeLayer && nodeLayer->layer()->isSpatial() && nodeLayer->isVisible()) { result << layer; } } return result; } QList resolveLayers(std::optional layerIds) { if (!layerIds.has_value() || layerIds->isUndefined() || layerIds->isNull()) return QgisApi_visibleLayers(); QList result; int length = (*layerIds)["length"].as(); for (int i = 0; i < length; i++) { std::string id = (*layerIds)[i].as(); QgsMapLayer *layer = QgsProject::instance()->mapLayer(QString::fromStdString(id)); if (layer) result << layer; } return result; } bool QgisApi_loadProject(std::string filename) { Qgis::ProjectReadFlags readFlags = Qgis::ProjectReadFlag::ForceReadOnlyLayers | Qgis::ProjectReadFlag::TrustLayerMetadata; bool res = QgsProject::instance()->read(QString::fromStdString(filename), readFlags); if (!res) return false; return true; } QgsRectangle QgisApi_fullExtent(std::optional layerIds) { QgsMapSettings mapSettings; mapSettings.setDestinationCrs(QgsProject::instance()->crs()); mapSettings.setLayers(resolveLayers(layerIds)); return mapSettings.fullExtent(); } std::string QgisApi_srid() { return QgsProject::instance()->crs().authid().toStdString(); } void QgisApi_renderXYZTile( unsigned long x, unsigned long y, unsigned int z, unsigned int tileSize, float pixelRatio, float extentBufferFactor, emscripten::val callback, std::optional layerIds) { QgsMapSettings mapSettings; mapSettings.setOutputImageFormat(QImage::Format_ARGB32); mapSettings.setBackgroundColor(Qt::transparent); mapSettings.setOutputSize(QSize(tileSize, tileSize)); mapSettings.setOutputDpi(96.0 * pixelRatio); mapSettings.setLayers(resolveLayers(layerIds)); mapSettings.setDestinationCrs(QgsCoordinateReferenceSystem(QStringLiteral("EPSG:3857"))); QgsTileMatrix mTileMatrix = QgsTileMatrix::fromWebMercator(z); auto extent = mTileMatrix.tileExtent(QgsTileXYZ(x, y, z)); mapSettings.setExtent(extent); auto tileExtentBuffer = extent.width() * extentBufferFactor; if (tileExtentBuffer > 0.0) { mapSettings.setExtentBuffer(tileExtentBuffer); } mapSettings.setFlag(Qgis::MapSettingsFlag::RenderMapTile, true); QgsExpressionContext context = QgsProject::instance()->createExpressionContext(); context << QgsExpressionContextUtils::mapSettingsScope(mapSettings); mapSettings.setExpressionContext(context); mapSettings.setPathResolver(QgsProject::instance()->pathResolver()); QgsMapRendererSequentialJob *job = new QgsMapRendererSequentialJob(mapSettings); QObject::connect(job, &QgsMapRendererSequentialJob::finished, [job, callback] { auto image = job->renderedImage(); image.rgbSwap(); // for html canvas callback(emscripten::val(emscripten::typed_memory_view( image.width() * image.height() * 4, (const unsigned char *)image.constBits()))); job->deleteLater(); }); job->start(); } void QgisApi_renderImage( std::string srid, const QgsRectangle &extent, unsigned int width, unsigned int height, float pixelRatio, emscripten::val callback, std::optional layerIds) { QgsMapSettings mapSettings; mapSettings.setOutputImageFormat(QImage::Format_ARGB32); mapSettings.setBackgroundColor(Qt::transparent); mapSettings.setOutputSize(QSize(width, height)); mapSettings.setOutputDpi(96.0 * pixelRatio); mapSettings.setLayers(resolveLayers(layerIds)); mapSettings.setDestinationCrs(QgsCoordinateReferenceSystem(QString::fromStdString(srid))); mapSettings.setExtent(extent); QgsExpressionContext context = QgsProject::instance()->createExpressionContext(); context << QgsExpressionContextUtils::mapSettingsScope(mapSettings); mapSettings.setExpressionContext(context); mapSettings.setPathResolver(QgsProject::instance()->pathResolver()); // START optimizations QgsVectorSimplifyMethod simplify; simplify.setSimplifyHints(Qgis::VectorRenderingSimplificationFlag::FullSimplification); mapSettings.setSimplifyMethod(simplify); mapSettings.setFlag(Qgis::MapSettingsFlag::UseRenderingOptimization, true); mapSettings.setFlag(Qgis::MapSettingsFlag::ForceRasterMasks, true); mapSettings.setFlag(Qgis::MapSettingsFlag::RenderPreviewJob, true); mapSettings.setRendererUsage(Qgis::RendererUsage::View); // END optimizations QgsMapRendererParallelJob *job = new QgsMapRendererParallelJob(mapSettings); QObject::connect(job, &QgsMapRendererParallelJob::finished, [job, callback] { auto image = job->renderedImage(); image.rgbSwap(); // for html canvas callback(emscripten::val(emscripten::typed_memory_view( image.width() * image.height() * 4, (const unsigned char *)image.constBits()))); job->deleteLater(); }); job->start(); } QgsMapRendererParallelJob *QgisApi_renderJob( std::string srid, const QgsRectangle &extent, unsigned int width, unsigned int height, float pixelRatio, std::optional layerIds) { QgsMapSettings mapSettings; mapSettings.setOutputImageFormat(QImage::Format_ARGB32); mapSettings.setBackgroundColor(Qt::transparent); mapSettings.setOutputSize(QSize(width, height)); mapSettings.setOutputDpi(96.0 * pixelRatio); mapSettings.setLayers(resolveLayers(layerIds)); mapSettings.setDestinationCrs(QgsCoordinateReferenceSystem(QString::fromStdString(srid))); mapSettings.setExtent(extent); QgsExpressionContext context = QgsProject::instance()->createExpressionContext(); context << QgsExpressionContextUtils::mapSettingsScope(mapSettings); mapSettings.setExpressionContext(context); mapSettings.setPathResolver(QgsProject::instance()->pathResolver()); // START optimizations QgsVectorSimplifyMethod simplify; simplify.setSimplifyHints(Qgis::VectorRenderingSimplificationFlag::FullSimplification); mapSettings.setSimplifyMethod(simplify); mapSettings.setFlag(Qgis::MapSettingsFlag::UseRenderingOptimization, true); mapSettings.setFlag(Qgis::MapSettingsFlag::ForceRasterMasks, true); mapSettings.setFlag(Qgis::MapSettingsFlag::RenderPreviewJob, true); mapSettings.setFlag(Qgis::MapSettingsFlag::RenderPartialOutput, true); mapSettings.setRendererUsage(Qgis::RendererUsage::View); // END optimizations QgsMapRendererParallelJob *job = new QgsMapRendererParallelJob(mapSettings); job->start(); return job; } const QgsRectangle QgisApi_transformRectangle( const QgsRectangle &inputRectangle, std::string inputSrid, std::string outputSrid) { QgsCoordinateTransform transform( QgsCoordinateReferenceSystem(QString::fromStdString(inputSrid)), QgsCoordinateReferenceSystem(QString::fromStdString(outputSrid)), QgsProject::instance()); return transform.transformBoundingBox(inputRectangle); } LayerTreeGroup QgisApi_layerTreeRoot() { return LayerTreeGroup(QgsProject::instance()->layerTreeRoot()); } const std::vector QgisApi_mapThemes() { std::vector result = {}; for (const QString &theme : QgsProject::instance()->mapThemeCollection()->mapThemes()) { result.push_back(theme.toStdString()); } return result; } const std::string QgisApi_getMapTheme() { QgsLayerTree *layerTreeRoot = QgsProject::instance()->layerTreeRoot(); QgsMapThemeCollection *collection = QgsProject::instance()->mapThemeCollection(); QgsLayerTreeModel model(layerTreeRoot); auto currentState = QgsMapThemeCollection::createThemeFromCurrentState(layerTreeRoot, &model); for (const QString &theme : QgsProject::instance()->mapThemeCollection()->mapThemes()) { if (currentState == QgsProject::instance()->mapThemeCollection()->mapThemeState(theme)) { return theme.toStdString(); } } return ""; } const bool QgisApi_setMapTheme(std::string themeName) { QString qThemeName = QString::fromStdString(themeName); if (!QgsProject::instance()->mapThemeCollection()->hasMapTheme(qThemeName)) { return false; } else { QgsLayerTree *layerTreeRoot = QgsProject::instance()->layerTreeRoot(); QgsMapThemeCollection *collection = QgsProject::instance()->mapThemeCollection(); QgsLayerTreeModel model(layerTreeRoot); collection->applyTheme(qThemeName, layerTreeRoot, &model); return true; } } emscripten::val QgisApi_globalVariables() { emscripten::val result = emscripten::val::object(); std::unique_ptr scope(QgsExpressionContextUtils::globalScope()); for (const QString &name : scope->variableNames()) { result.set(name.toStdString(), scope->variable(name).toString().toStdString()); } return result; } emscripten::val QgisApi_projectVariables() { emscripten::val result = emscripten::val::object(); std::unique_ptr scope( QgsExpressionContextUtils::projectScope(QgsProject::instance())); for (const QString &name : scope->variableNames()) { result.set(name.toStdString(), scope->variable(name).toString().toStdString()); } return result; } void QgisApi_setGlobalVariables(emscripten::val variables) { QVariantMap vars; emscripten::val keys = emscripten::val::global("Object").call("keys", variables); int length = keys["length"].as(); for (int i = 0; i < length; i++) { std::string key = keys[i].as(); std::string value = variables[key].as(); vars.insert(QString::fromStdString(key), QString::fromStdString(value)); } QgsExpressionContextUtils::setGlobalVariables(vars); } void QgisApi_setProjectVariables(emscripten::val variables) { QVariantMap vars; emscripten::val keys = emscripten::val::global("Object").call("keys", variables); int length = keys["length"].as(); for (int i = 0; i < length; i++) { std::string key = keys[i].as(); std::string value = variables[key].as(); vars.insert(QString::fromStdString(key), QString::fromStdString(value)); } QgsExpressionContextUtils::setProjectVariables(QgsProject::instance(), vars); } std::string QgisApi_renderLegend(float dpi, std::optional layerIds) { if (!layerIds.has_value() || layerIds->isUndefined() || layerIds->isNull()) { return renderLegendForTree(QgsProject::instance()->layerTreeRoot(), dpi); } QgsLayerTree tempRoot; int length = (*layerIds)["length"].as(); for (int i = 0; i < length; i++) { std::string id = (*layerIds)[i].as(); QgsMapLayer *layer = QgsProject::instance()->mapLayer(QString::fromStdString(id)); if (layer) tempRoot.addLayer(layer); } return renderLegendForTree(&tempRoot, dpi); } struct LayerDefinitionResult { bool success; std::string errorMessage; }; LayerDefinitionResult QgisApi_loadLayerDefinition(std::string path, std::optional targetGroup) { QString errorMessage; QgsLayerTreeGroup *group = QgsProject::instance()->layerTreeRoot(); if (targetGroup.has_value() && targetGroup->isValid()) { group = static_cast(targetGroup->nativeNode()); } bool ok = QgsLayerDefinition::loadLayerDefinition( QString::fromStdString(path), QgsProject::instance(), group, errorMessage); return {ok, errorMessage.toStdString()}; } EMSCRIPTEN_BINDINGS(QgisApi) { emscripten::value_object("LayerDefinitionResult") .field("success", &LayerDefinitionResult::success) .field("errorMessage", &LayerDefinitionResult::errorMessage); emscripten::function("loadProject", &QgisApi_loadProject); emscripten::function("fullExtent", &QgisApi_fullExtent); emscripten::function("srid", &QgisApi_srid); emscripten::function("renderImage", &QgisApi_renderImage); emscripten::function("renderXYZTile", &QgisApi_renderXYZTile); emscripten::function("renderJob", &QgisApi_renderJob, emscripten::allow_raw_pointers()); emscripten::function("transformRectangle", &QgisApi_transformRectangle); emscripten::function("layerTreeRoot", &QgisApi_layerTreeRoot); emscripten::function("mapThemes", &QgisApi_mapThemes); emscripten::function("getMapTheme", &QgisApi_getMapTheme); emscripten::function("setMapTheme", &QgisApi_setMapTheme); emscripten::register_vector("vector"); emscripten::register_optional(); emscripten::register_optional(); emscripten::function("globalVariables", &QgisApi_globalVariables); emscripten::function("projectVariables", &QgisApi_projectVariables); emscripten::function("setGlobalVariables", &QgisApi_setGlobalVariables); emscripten::function("setProjectVariables", &QgisApi_setProjectVariables); emscripten::function("renderLegend", &QgisApi_renderLegend); emscripten::function("loadLayerDefinition", &QgisApi_loadLayerDefinition); } ================================================ FILE: src/api/QgisApi.ts ================================================ import { QgisModelConstructors, QgsMapRendererParallelJob, QgsRectangle, QgsLayerTreeGroup, } from "./QgisModel"; export interface LayerDefinitionResult { success: boolean; errorMessage: string; } /** * Common QGIS API which exposes the {@link QgisModelConstructors} and methods that can be accessed from both the public {@link QgisApi} and the {@link InternalQgisApi}. */ export interface CommonQgisApi extends QgisModelConstructors { /** * Loads a QGIS project file. * * @param filename - The path to the QGIS project file. * @returns true if the project was loaded successfully, false otherwise. */ loadProject(filename: string): boolean; /** * Loads a QGIS Layer Definition (.qlr) file into the project. * * @param path - Path to the .qlr file on the virtual filesystem. * @param targetGroup - Optional target group to load into. Defaults to the root group. * @returns Result indicating success or failure with an error message. */ loadLayerDefinition( path: string, targetGroup?: QgsLayerTreeGroup, ): LayerDefinitionResult; /** * Returns the full extent of the loaded project. * * @returns The full extent of the loaded project. */ fullExtent(layerIds?: string[]): QgsRectangle; /** * Returns the SRID of the loaded project. * * @returns The SRID of the loaded project. */ srid(): string; /** * Transforms a rectangle from one SRID to another. * * @param rect - The rectangle to transform. * @param inputSrid - The SRID of the input rectangle. * @param outputSrid - The SRID of the output rectangle. * @returns The transformed rectangle. */ transformRectangle( rect: QgsRectangle, inputSrid: string, outputSrid: string, ): QgsRectangle; /** * Gets the current map theme of the current project. * * @returns The name of the current map theme. An empty string if no map theme is set. */ getMapTheme(): string; /** * Sets a map theme of the current project. * * @param * @returns true if the map theme was loaded successfully, false otherwise. */ setMapTheme(mapTheme: string): boolean; /** * Returns the global expression variables. * * These are system-level variables such as `qgis_version`, `qgis_locale`, * `qgis_os_name`, `qgis_platform`, etc. * * @returns A record of variable names to their string values. */ globalVariables(): Record; /** * Returns the project expression variables. * * These are project-level variables such as `project_crs`, `project_title`, * `project_filename`, `layer_ids`, `project_area_units`, etc. * Only available after a project has been loaded. * * @returns A record of variable names to their string values. */ projectVariables(): Record; /** * Sets global expression variables. * * Custom variables that will be available in all expression contexts. * Note: This replaces all custom global variables. * * @param variables - A record of variable names to their string values. */ setGlobalVariables(variables: Record): void; /** * Sets project expression variables. * * Custom variables that will be available in the project's expression contexts. * Note: This replaces all custom project variables. * * @param variables - A record of variable names to their string values. */ setProjectVariables(variables: Record): void; /** * Returns the root of the project's layer tree. * * @returns The root layer tree node. */ layerTreeRoot(): QgsLayerTreeGroup; /** * Renders the full project legend as a PNG image. * * @param dpi - The DPI for rendering the legend. * @returns A base64 data URL of the legend PNG, or an empty string if the legend is empty. */ renderLegend(dpi: number, layerIds?: string[]): string; /** * Renders an image of the loaded project and provides a QgsMapRendererParallelJob object to monitor the rendering progress and to retrieve preview images. * * @param srid - The SRID of the image. * @param extent - The extent of the image. * @param width - The width of the image. * @param height - The height of the image. * @param pixelRatio - The optional pixel ratio of the image which defaults to 1. * @returns A QgsMapRendererParallelJob object that can be used to monitor the rendering progress and to retrieve preview images. */ renderJob( srid: string, extent: QgsRectangle, width: number, height: number, pixelRatio: number, layerIds?: string[], ): QgsMapRendererParallelJob; } /** * The QgisApiAdapter provides convenience methods in addition to the CommonQgisApi. */ export interface QgisApiAdapter { /** * Renders an image of the loaded project. * * @param srid - The SRID of the image. * @param extent - The extent of the image. * @param width - The width of the image. * @param height - The height of the image. * @param pixelRatio - The optional pixel ratio of the image which defaults to 1. * @returns The rendered tile as an ImageData object. */ renderImage( srid: string, extent: QgsRectangle, width: number, height: number, pixelRatio?: number, layerIds?: string[], ): Promise; /** * Renders a tile of the loaded project in the XYZ scheme (EPSG:3857). * * @param x - The x coordinate of the tile. * @param y - The y coordinate of the tile. * @param z - The z coordinate of the tile. * @param tileSize - The optional size of the tile which defaults to 256. * @param pixelRatio - The optional pixel ratio of the tile which defaults to 1. * @param extentBufferFactor - The optional extent buffer factor of the tile which defaults to 0. * @param layerIds - Optional array of layer IDs to render. If omitted, renders all visible layers. * @returns The rendered tile as an ImageData object. */ renderXYZTile( x: number, y: number, z: number, tileSize?: number, pixelRatio?: number, extentBufferFactor?: number, layerIds?: string[], ): Promise; /** * Returns the map themes of the loaded project. * * @returns The map themes of the loaded project. */ mapThemes(): readonly string[]; } /** * Interface representing the public QgisApi. */ export interface QgisApi extends CommonQgisApi, QgisApiAdapter {} /** * The internal Qgis API which can be accessed from the QgisRuntimeModule * * This interface is not a stable API and may change at any time. Use the public {@link QgisApi} instead. */ export interface InternalQgisApi extends CommonQgisApi { renderImage( srid: string, extent: QgsRectangle, width: number, height: number, pixelRatio: number, callback: (tileData: ArrayBufferLike) => void, layerIds?: string[], ): void; renderXYZTile( x: number, y: number, z: number, tileSize: number, pixelRatio: number, extentBuffer: number, callback: (tileData: ArrayBufferLike) => void, layerIds?: string[], ): number; mapThemes(): any; } ================================================ FILE: src/api/QgisModel.ts ================================================ import type { QgsPointXY, QgsPointXYConstructors } from "../model/QgsPointXY"; import type { QgsRectangle, QgsRectangleConstructors, } from "../model/QgsRectangle"; import type { QgsLayerTreeModelLegendNode } from "../model/QgsLayerTreeModelLegendNode"; import type { QgsLayerTreeNode } from "../model/QgsLayerTreeNode"; import { NodeType } from "../model/QgsLayerTreeNode"; import type { QgsLayerTreeGroup } from "../model/QgsLayerTreeGroup"; import type { QgsLayerTreeLayer } from "../model/QgsLayerTreeLayer"; import type { QgsMapLayer, QgsVectorLayer } from "../model/QgsMapLayer"; import { LayerType } from "../model/QgsMapLayer"; import type { QgsMapRendererParallelJob } from "../model/QgsMapRendererParallelJob"; import type { QgsMapRendererJob } from "../model/QgsMapRendererJob"; import type { QgsMapRendererQImageJob } from "../model/QgsMapRendererQImageJob"; export type { QgsMapRendererJob, QgsMapRendererQImageJob, QgsMapRendererParallelJob, QgsPointXY, QgsRectangle, QgsLayerTreeModelLegendNode, QgsLayerTreeNode, QgsLayerTreeGroup, QgsLayerTreeLayer, QgsMapLayer, QgsVectorLayer, }; export type { LayerDefinitionResult } from "./QgisApi"; export { LayerType, NodeType }; /* prettier-ignore */ export interface QgisModelConstructors extends QgsPointXYConstructors, QgsRectangleConstructors {} ================================================ FILE: src/model/QgsLayerTreeGroup.hpp ================================================ #pragma once #include #include #include "./QgsLayerTreeModelLegendNode.hpp" #include "./QgsLayerTreeNode.hpp" class LayerTreeGroup : public LayerTreeNode { public: LayerTreeGroup() : LayerTreeNode(nullptr) {} LayerTreeGroup(QgsLayerTreeGroup *group) : LayerTreeNode(group) {} emscripten::val findLayers() const { emscripten::val result = emscripten::val::array(); if (auto *group = asGroup()) { for (QgsLayerTreeLayer *layerNode : group->findLayers()) { result.call("push", wrapNode(layerNode)); } } return result; } emscripten::val findGroup(std::string name) const { if (auto *group = asGroup()) { QgsLayerTreeGroup *found = group->findGroup(QString::fromStdString(name)); if (found) return wrapNode(found); } return wrapNode(nullptr); } emscripten::val findLayer(std::string layerId) const { if (auto *group = asGroup()) { QgsLayerTreeLayer *found = group->findLayer(QString::fromStdString(layerId)); if (found) return wrapNode(found); } return wrapNode(nullptr); } bool isMutuallyExclusive() const { if (auto *group = asGroup()) return group->isMutuallyExclusive(); return false; } void setIsMutuallyExclusive(bool exclusive) { if (auto *group = asGroup()) group->setIsMutuallyExclusive(exclusive); } std::string renderLegend(float dpi) const { auto *group = asGroup(); if (!group) return ""; QgsLayerTree tempRoot; tempRoot.addChildNode(group->clone()); return renderLegendForTree(&tempRoot, dpi); } private: QgsLayerTreeGroup *asGroup() const { Q_ASSERT(_node && _node->nodeType() == QgsLayerTreeNode::NodeGroup); return static_cast(_node); } }; EMSCRIPTEN_BINDINGS(QgsLayerTreeGroup) { emscripten::class_>("QgsLayerTreeGroup") .function("findLayers", &LayerTreeGroup::findLayers) .function("findGroup", &LayerTreeGroup::findGroup) .function("findLayer", &LayerTreeGroup::findLayer) .property( "isMutuallyExclusive", &LayerTreeGroup::isMutuallyExclusive, &LayerTreeGroup::setIsMutuallyExclusive) .function("renderLegend", &LayerTreeGroup::renderLegend); } ================================================ FILE: src/model/QgsLayerTreeGroup.ts ================================================ import type { QgsLayerTreeNode } from "./QgsLayerTreeNode"; import type { QgsLayerTreeLayer } from "./QgsLayerTreeLayer"; export interface QgsLayerTreeGroup extends QgsLayerTreeNode { isMutuallyExclusive: boolean; findLayers(): QgsLayerTreeLayer[]; findGroup(name: string): QgsLayerTreeGroup | null; findLayer(layerId: string): QgsLayerTreeLayer | null; renderLegend(dpi: number): string; } ================================================ FILE: src/model/QgsLayerTreeLayer.hpp ================================================ #pragma once #include #include #include #include "./QgsLayerTreeGroup.hpp" #include "./QgsMapLayer.hpp" class LayerTreeLayer : public LayerTreeNode { public: LayerTreeLayer() : LayerTreeNode(nullptr) {} LayerTreeLayer(QgsLayerTreeLayer *layer) : LayerTreeNode(layer) {} std::string layerId() const { if (auto *layer = asLayer()) return layer->layerId().toStdString(); return ""; } emscripten::val layer() const { if (auto *treeLayer = asLayer()) return wrapLayer(treeLayer->layer()); return emscripten::val::null(); } std::string renderLegend(float dpi) const { auto *treeLayer = asLayer(); if (!treeLayer || !treeLayer->layer()) return ""; QgsLayerTree tempRoot; tempRoot.addLayer(treeLayer->layer()); return renderLegendForTree(&tempRoot, dpi); } emscripten::val legendNodes() const { emscripten::val result = emscripten::val::array(); auto *treeLayer = asLayer(); if (!treeLayer || !treeLayer->layer() || !treeLayer->layer()->legend()) return result; qDeleteAll(legendNodesCache()); legendNodesCache() = treeLayer->layer()->legend()->createLayerTreeModelLegendNodes(treeLayer); for (auto *node : legendNodesCache()) { result.call("push", LegendNode(node)); } return result; } private: QgsLayerTreeLayer *asLayer() const { Q_ASSERT(_node && _node->nodeType() == QgsLayerTreeNode::NodeLayer); return static_cast(_node); } }; inline emscripten::val wrapNode(QgsLayerTreeNode *node) { if (!node) return emscripten::val::null(); if (QgsLayerTree::isGroup(node)) return emscripten::val(LayerTreeGroup(static_cast(node))); return emscripten::val(LayerTreeLayer(static_cast(node))); } EMSCRIPTEN_BINDINGS(QgsLayerTreeLayer) { emscripten::class_>("QgsLayerTreeLayer") .function("layerId", &LayerTreeLayer::layerId) .function("layer", &LayerTreeLayer::layer) .function("legendNodes", &LayerTreeLayer::legendNodes) .function("renderLegend", &LayerTreeLayer::renderLegend); } ================================================ FILE: src/model/QgsLayerTreeLayer.ts ================================================ import type { QgsLayerTreeModelLegendNode } from "./QgsLayerTreeModelLegendNode"; import type { QgsLayerTreeNode } from "./QgsLayerTreeNode"; import type { QgsMapLayer } from "./QgsMapLayer"; export interface QgsLayerTreeLayer extends QgsLayerTreeNode { layerId(): string; layer(): QgsMapLayer | null; legendNodes(): QgsLayerTreeModelLegendNode[]; renderLegend(dpi: number): string; } ================================================ FILE: src/model/QgsLayerTreeModelLegendNode.hpp ================================================ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include inline QList &legendNodesCache() { static QList cache; return cache; } inline std::string imageToDataUrl(const QImage &image) { if (image.isNull()) return ""; QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); image.save(&buffer, "PNG"); return "data:image/png;base64," + ba.toBase64().toStdString(); } inline std::string renderLegendForTree(QgsLayerTree *tree, float dpi) { QgsLayerTreeModel model(tree); QgsLegendSettings settings; settings.setTitle(""); QgsRenderContext context; context.setScaleFactor(dpi / 25.4); QgsLegendRenderer renderer(&model, settings); QSizeF minSize = renderer.minimumSize(&context); int width = static_cast(std::ceil(minSize.width() * dpi / 25.4)); int height = static_cast(std::ceil(minSize.height() * dpi / 25.4)); if (width <= 0 || height <= 0) return ""; QImage image(width, height, QImage::Format_ARGB32); image.fill(Qt::transparent); image.setDotsPerMeterX(static_cast(dpi / 25.4 * 1000)); image.setDotsPerMeterY(static_cast(dpi / 25.4 * 1000)); QPainter painter(&image); painter.setRenderHint(QPainter::Antialiasing, true); painter.scale(dpi / 25.4, dpi / 25.4); QgsRenderContext renderContext = QgsRenderContext::fromQPainter(&painter); renderContext.setScaleFactor(dpi / 25.4); renderer.setLegendSize(minSize); renderer.drawLegend(renderContext); painter.end(); return imageToDataUrl(image); } class LegendNode { public: LegendNode() : _node(nullptr) {} LegendNode(QgsLayerTreeModelLegendNode *node) : _node(node) {} bool isValid() const { return _node != nullptr; } std::string label() const { if (!_node) return ""; return _node->data(Qt::DisplayRole).toString().toStdString(); } std::string symbolImage(int size) const { if (!_node) return ""; if (auto *symNode = dynamic_cast(_node)) { if (const QgsSymbol *symbol = symNode->symbol()) { QImage img = const_cast(symbol)->asImage(QSize(size, size)); return imageToDataUrl(img); } } QVariant iconData = _node->data(Qt::DecorationRole); if (iconData.canConvert()) { QIcon icon = iconData.value(); QPixmap pix = icon.pixmap(QSize(size, size)); if (!pix.isNull()) return imageToDataUrl(pix.toImage()); } return ""; } private: QgsLayerTreeModelLegendNode *_node; }; EMSCRIPTEN_BINDINGS(QgsLayerTreeModelLegendNode) { emscripten::class_("QgsLayerTreeModelLegendNode") .constructor<>() .function("isValid", &LegendNode::isValid) .function("label", &LegendNode::label) .function("symbolImage", &LegendNode::symbolImage); } ================================================ FILE: src/model/QgsLayerTreeModelLegendNode.ts ================================================ export interface QgsLayerTreeModelLegendNode { isValid(): boolean; label(): string; symbolImage(size: number): string; } ================================================ FILE: src/model/QgsLayerTreeNode.hpp ================================================ #pragma once #include #include #include #include class LayerTreeGroup; class LayerTreeLayer; emscripten::val wrapNode(QgsLayerTreeNode *node); class LayerTreeNode { public: LayerTreeNode() : _node(nullptr) {} LayerTreeNode(QgsLayerTreeNode *node) : _node(node) {} virtual ~LayerTreeNode() = default; bool isValid() const { return _node != nullptr; } int nodeType() const { if (!_node) return -1; return static_cast(_node->nodeType()); } bool isGroup() const { return _node && _node->nodeType() == QgsLayerTreeNode::NodeGroup; } bool isLayer() const { return _node && _node->nodeType() == QgsLayerTreeNode::NodeLayer; } std::string name() const { if (!_node) return ""; return _node->name().toStdString(); } void setName(std::string name) { if (_node) _node->setName(QString::fromStdString(name)); } bool isVisible() const { if (!_node) return false; return _node->isVisible(); } bool itemVisibilityChecked() const { if (!_node) return false; return _node->itemVisibilityChecked(); } void setItemVisibilityChecked(bool checked) { if (_node) _node->setItemVisibilityChecked(checked); } bool isExpanded() const { if (!_node) return false; return _node->isExpanded(); } void setExpanded(bool expanded) { if (_node) _node->setExpanded(expanded); } emscripten::val children() const { emscripten::val result = emscripten::val::array(); if (!_node) return result; for (QgsLayerTreeNode *child : _node->children()) { result.call("push", wrapNode(child)); } return result; } QgsLayerTreeNode *nativeNode() const { return _node; } protected: QgsLayerTreeNode *_node; }; EMSCRIPTEN_BINDINGS(QgsLayerTreeNode) { emscripten::class_("QgsLayerTreeNode") .constructor<>() .function("isValid", &LayerTreeNode::isValid) .function("nodeType", &LayerTreeNode::nodeType) .function("isGroup", &LayerTreeNode::isGroup) .function("isLayer", &LayerTreeNode::isLayer) .property("name", &LayerTreeNode::name, &LayerTreeNode::setName) .function("isVisible", &LayerTreeNode::isVisible) .property( "itemVisibilityChecked", &LayerTreeNode::itemVisibilityChecked, &LayerTreeNode::setItemVisibilityChecked) .property("expanded", &LayerTreeNode::isExpanded, &LayerTreeNode::setExpanded) .function("children", &LayerTreeNode::children); } ================================================ FILE: src/model/QgsLayerTreeNode.ts ================================================ import type { QgsLayerTreeGroup } from "./QgsLayerTreeGroup"; import type { QgsLayerTreeLayer } from "./QgsLayerTreeLayer"; export enum NodeType { NodeGroup = 0, NodeLayer = 1, } export interface QgsLayerTreeNode { name: string; itemVisibilityChecked: boolean; expanded: boolean; isValid(): boolean; nodeType(): NodeType; isGroup(): boolean; isLayer(): boolean; isVisible(): boolean; children(): (QgsLayerTreeGroup | QgsLayerTreeLayer)[]; } ================================================ FILE: src/model/QgsMapLayer.hpp ================================================ #pragma once #include #include #include #include #include class VectorLayer; emscripten::val wrapLayer(QgsMapLayer *layer); class MapLayer { public: MapLayer() : _layer(nullptr) {} MapLayer(QgsMapLayer *layer) : _layer(layer) {} virtual ~MapLayer() = default; bool isValid() const { return _layer != nullptr; } int type() const { if (!_layer) return -1; return static_cast(_layer->type()); } std::string id() const { if (!_layer) return ""; return _layer->id().toStdString(); } std::string name() const { if (!_layer) return ""; return _layer->name().toStdString(); } void setName(std::string name) { if (_layer) _layer->setName(QString::fromStdString(name)); } double opacity() const { if (!_layer) return 1.0; return _layer->opacity(); } void setOpacity(double opacity) { if (_layer) _layer->setOpacity(opacity); } protected: QgsMapLayer *_layer; }; class VectorLayer : public MapLayer { public: VectorLayer() : MapLayer(nullptr) {} VectorLayer(QgsVectorLayer *layer) : MapLayer(layer) {} std::string subsetString() const { if (auto *vl = asVectorLayer()) return vl->subsetString().toStdString(); return ""; } bool setSubsetString(std::string subset) { if (auto *vl = asVectorLayer()) return vl->setSubsetString(QString::fromStdString(subset)); return false; } private: QgsVectorLayer *asVectorLayer() const { Q_ASSERT(_layer && _layer->type() == Qgis::LayerType::Vector); return static_cast(_layer); } }; inline emscripten::val wrapLayer(QgsMapLayer *layer) { if (!layer) return emscripten::val::null(); if (layer->type() == Qgis::LayerType::Vector) return emscripten::val(VectorLayer(static_cast(layer))); return emscripten::val(MapLayer(layer)); } EMSCRIPTEN_BINDINGS(QgsMapLayer) { emscripten::class_("QgsMapLayer") .constructor<>() .function("isValid", &MapLayer::isValid) .function("type", &MapLayer::type) .function("id", &MapLayer::id) .property("name", &MapLayer::name, &MapLayer::setName) .property("opacity", &MapLayer::opacity, &MapLayer::setOpacity); emscripten::class_>("QgsVectorLayer") .function("subsetString", &VectorLayer::subsetString) .function("setSubsetString", &VectorLayer::setSubsetString); } ================================================ FILE: src/model/QgsMapLayer.ts ================================================ export enum LayerType { Vector = 0, Raster = 1, Plugin = 2, Mesh = 3, VectorTile = 4, Annotation = 5, PointCloud = 6, Group = 7, TiledScene = 8, } export interface QgsMapLayer { name: string; opacity: number; isValid(): boolean; type(): LayerType; id(): string; } export interface QgsVectorLayer extends QgsMapLayer { subsetString(): string; setSubsetString(subset: string): boolean; } ================================================ FILE: src/model/QgsMapRendererJob.hpp ================================================ #pragma once #include #include static void QgsMapRendererJob_finished(QgsMapRendererJob &job, emscripten::val callback) { QObject::connect(&job, &QgsMapRendererJob::finished, [callback] { callback(); }); } EMSCRIPTEN_BINDINGS(QgsMapRendererJob) { emscripten::class_("QgsMapRendererJob") .function("cancel", &QgsMapRendererJob::cancel) .function("cancelWithoutBlocking", &QgsMapRendererJob::cancelWithoutBlocking) .function("isActive", &QgsMapRendererJob::isActive) .function("renderingTime", &QgsMapRendererJob::renderingTime) // signals .function("finished", &QgsMapRendererJob_finished); } ================================================ FILE: src/model/QgsMapRendererJob.ts ================================================ /** * Abstract base class for map rendering implementations * * {@link https://api.qgis.org/api/classQgsMapRendererJob.html} */ export interface QgsMapRendererJob { /** * Stop the rendering job - does not return until the job has terminated */ cancel(): void; /** * Triggers cancellation of the rendering job without blocking. */ cancelWithoutBlocking(): void; /** * Tell whether the rendering job is currently running in background. * @returns True if the job is active, false otherwise. */ isActive(): boolean; /** * Returns the total time it took to finish the job (in milliseconds) * @returns The rendering time in milliseconds. */ renderingTime(): number; /** * Registers a callback function to be called when the map rendering job is finished. * @param callback The callback function to be called. */ finished(callback: () => void): void; } ================================================ FILE: src/model/QgsMapRendererParallelJob.hpp ================================================ #pragma once #include #include #include "./QgsMapRendererQImageJob.hpp" EMSCRIPTEN_BINDINGS(QgsMapRendererParallelJob) { emscripten::class_>( "QgsMapRendererParallelJob") .function("cancel", &QgsMapRendererParallelJob::cancel) .function("cancelWithoutBlocking", &QgsMapRendererParallelJob::cancelWithoutBlocking) .function("isActive", &QgsMapRendererParallelJob::isActive) .function("renderingTime", &QgsMapRendererParallelJob::renderingTime); } ================================================ FILE: src/model/QgsMapRendererParallelJob.ts ================================================ import { QgsMapRendererQImageJob } from "./QgsMapRendererQImageJob"; /** * Job implementation that renders all layers in parallel * * {@link https://api.qgis.org/api/classQgsMapRendererParallelJob.html} */ export interface QgsMapRendererParallelJob extends QgsMapRendererQImageJob {} ================================================ FILE: src/model/QgsMapRendererQImageJob.hpp ================================================ #pragma once #include #include #include "./QgsMapRendererJob.hpp" static emscripten::val QgsMapRendererQImageJob_renderedImage(QgsMapRendererQImageJob &job) { auto image = std::move(job.renderedImage()).convertToFormat(QImage::Format_RGBA8888); return emscripten::val(emscripten::typed_memory_view( image.width() * image.height() * 4, (const unsigned char *)image.constBits())); } EMSCRIPTEN_BINDINGS(QgsMapRendererQImageJob) { emscripten::class_>( "QgsMapRendererQImageJob") .function("renderedImage", &QgsMapRendererQImageJob_renderedImage); } ================================================ FILE: src/model/QgsMapRendererQImageJob.ts ================================================ import { QgsMapRendererJob } from "./QgsMapRendererJob"; /** * Intermediate base class adding functionality that allows client to query the rendered image * * {@link https://api.qgis.org/api/classQgsMapRendererQImageJob.html} */ export interface QgsMapRendererQImageJob extends QgsMapRendererJob { /** * Returns the (preview or final) rendered image as a byte array * @returns The rendered image as an emscripten typed_memory_view. */ renderedImage(): ArrayBufferLike; } ================================================ FILE: src/model/QgsPointXY.hpp ================================================ #include #include EMSCRIPTEN_BINDINGS(QgsPointXY) { emscripten::class_("QgsPointXY") .constructor<>() .property("x", &QgsPointXY::x, &QgsPointXY::setX) .property("y", &QgsPointXY::y, &QgsPointXY::setY); } ================================================ FILE: src/model/QgsPointXY.ts ================================================ /** * A class to represent a 2D point. * * {@link https://api.qgis.org/api/classQgsPointXY.html} */ export interface QgsPointXY { x: number; y: number; } /** * The {@link QgsPointXY} constructors. */ export interface QgsPointXYConstructors { QgsPointXY: { new (): QgsPointXY }; } ================================================ FILE: src/model/QgsRectangle.hpp ================================================ #include #include static void QgsRectangle_scale(QgsRectangle &r, double f) { r.scale(f); } static void QgsRectangle_move(QgsRectangle &r, double dx, double dy) { r += QgsVector(dx, dy); } EMSCRIPTEN_BINDINGS(QgsRectangle) { emscripten::class_("QgsRectangle") .constructor<>() .constructor() .property("xMinimum", &QgsRectangle::xMinimum, &QgsRectangle::setXMinimum) .property("yMinimum", &QgsRectangle::yMinimum, &QgsRectangle::setYMinimum) .property("xMaximum", &QgsRectangle::xMaximum, &QgsRectangle::setXMaximum) .property("yMaximum", &QgsRectangle::yMaximum, &QgsRectangle::setYMaximum) .function("scale", &QgsRectangle_scale) .function("move", &QgsRectangle_move) .function("center", &QgsRectangle::center); } ================================================ FILE: src/model/QgsRectangle.ts ================================================ import { QgsPointXY } from "./QgsPointXY"; /** * A rectangle specified with double values. * * {@link https://api.qgis.org/api/classQgsRectangle.html} */ export interface QgsRectangle { xMaximum: number; xMinimum: number; yMaximum: number; yMinimum: number; scale(factor: number): void; move(dx: number, dy: number): void; center(): QgsPointXY; } /** * The {@link QgsRectangle} constructors. */ export interface QgsRectangleConstructors { QgsRectangle: { new (): QgsRectangle; new (xMin: number, yMin: number, xMax: number, yMax: number): QgsRectangle; }; } ================================================ FILE: src/qgis-js.cpp ================================================ #include "cpl_conv.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static const QTemporaryDir temp; static QGuiApplication *app; static const bool testLibraries = false; int main(int argc, char *argv[]) { // set PROJ search paths const char *path = "/proj"; proj_context_set_search_paths(nullptr, 1, &path); // START optimizations sqlite3_vfs_register(sqlite3_vfs_find("unix-none"), 1); CPLSetConfigOption("DO_NOT_ENABLE_WAL", "YES"); CPLSetConfigOption("OGR_SQLITE_JOURNAL", "OFF"); CPLSetConfigOption("OGR_SQLITE_CACHE", "128"); CPLSetConfigOption("OGR_SQLITE_SYNCHRONOUS", "OFF"); CPLSetConfigOption("NOLOCK", "YES"); CPLSetConfigOption("IMMUTABLE", "YES"); // END optimizations // needed? QCoreApplication::setOrganizationName("QGIS"); QCoreApplication::setOrganizationDomain("qgis.org"); QCoreApplication::setApplicationName("qgis-js"); // prevent warning from QWasmLocalStorageSettingsPrivate QSettings::setDefaultFormat(QSettings::IniFormat); QSettings::setPath( QSettings::IniFormat, QSettings::UserScope, temp.path() + QString("/settings")); app = new QGuiApplication(argc, argv); // qDebug() << "QgsApplication::init"; QgsApplication::init(temp.path()); QgsApplication::setPkgDataPath("/qgis"); // as set in CMakeLists.txt int maxThreads = 4; emscripten::val qgisJsMaxThreads = emscripten::val::module_property("qgisJsMaxThreads"); if (!qgisJsMaxThreads.isUndefined()) { int maxThreadsValue = qgisJsMaxThreads.as(); if (maxThreadsValue > 0) { maxThreads = maxThreadsValue; } } QgsApplication::setMaxThreads(maxThreads); QgsSettingsRegistryCore::settingsLayerParallelLoading->setValue(false); if (testLibraries) { // version information qDebug() << "qgis " << Qgis::version() << " (" << Qgis::devVersion() << ")"; qDebug() << "qt " << QString(QT_VERSION_STR); qDebug() << "gdal " << QString(GDAL_RELEASE_NAME); PJ_INFO info = proj_info(); const QString projVersionCompiled{QStringLiteral("%1.%2.%3") .arg(PROJ_VERSION_MAJOR) .arg(PROJ_VERSION_MINOR) .arg(PROJ_VERSION_PATCH)}; qDebug() << "proj " << projVersionCompiled; qDebug() << "geos " << QString(GEOS_CAPI_VERSION); qDebug() << "sqlite " << QString{SQLITE_VERSION}; // no PDAL // no postgres // no spatialite qDebug() << "OS " << QSysInfo::prettyProductName(); // says emscripten + version #ifdef QGISDEBUG qDebug() << "debug build of qgis"; #endif qDebug() << "qgis pkg data path: " << QgsApplication::pkgDataPath(); qDebug() << "srs path" << QgsApplication::srsDatabaseFilePath(); qDebug() << "proj search paths:" << QgsProjUtils::searchPaths(); // qgis providers QStringList providerList = QgsProviderRegistry::instance()->providerList(); qDebug() << "providers" << providerList; // gdal drivers int driverCount = GDALGetDriverCount(); QStringList gdalDrivers; // qDebug() << "gdal drivers" << driverCount; for (int i = 0; i < driverCount; ++i) { GDALDriverH dr = GDALGetDriver(i); QString driverName = GDALGetDescription(dr); gdalDrivers << driverName; // QString longName = GDALGetMetadataItem( dr, "DMD_LONGNAME", "" ); // bool isRaster = QString( GDALGetMetadataItem( dr, GDAL_DCAP_RASTER, // nullptr ) ) == QLatin1String( "YES" ); bool isVector = QString( // GDALGetMetadataItem( dr, GDAL_DCAP_VECTOR, nullptr ) ) == // QLatin1String( "YES" ); qDebug() << driverName << isRaster << isVector // << longName; // quite verbose with 100+ drivers! } qDebug() << "gdal drivers " << gdalDrivers; if (!QFileInfo::exists(QgsApplication::srsDatabaseFilePath())) { qDebug() << "srs db does not exist!" << QgsApplication::srsDatabaseFilePath(); return 1; } if (!QFileInfo::exists("/proj/proj.db")) { qDebug() << "proj db does not exist!" << "/proj/proj.db"; return 1; } if (QgsCoordinateReferenceSystem("EPSG:27700").toWkt().isEmpty()) { qDebug() << "something wrong with CRS database!"; return 1; } } return 0; } ================================================ FILE: src/qt.conf ================================================ [Paths] Prefix=/ ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES2022", "useDefineForClassFields": true, "module": "ESNext", "lib": ["ES2022", "DOM", "DOM.Iterable"], "skipLibCheck": true, /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, /* Linting */ "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noFallthroughCasesInSwitch": true } } ================================================ FILE: vcpkg.json ================================================ { "name": "qgis-js", "version": "0.1.0", "vcpkg-configuration": { "overlay-triplets": ["./build/vcpkg-triplets"], "overlay-ports": ["./build/vcpkg-ports"] }, "dependencies": [ "geos", { "name": "sqlite3", "default-features": false, "features": ["rtree", "json1", "fts3"] }, { "name": "proj", "default-features": false, "features": ["tiff"] }, { "name": "gdal", "default-features": false, "features": ["geos", "sqlite3"] }, "libspatialindex", "expat", { "name": "libzip", "default-features": false }, "protobuf", { "name": "freetype", "default-features": false, "features": ["zlib"] }, { "name": "freetype", "default-features": false, "host": true }, { "name": "qtbase", "default-features": false, "features": [ "opengl", "gles3", "concurrent", "freetype", "gui", "jpeg", "network", "widgets", "sql", "sql-sqlite", "testlib" ] }, { "name": "qtbase", "host": true, "default-features": false }, { "name": "qt5compat", "default-features": false, "features": ["textcodec"] }, { "name": "qtkeychain-qt6", "default-features": false }, { "name": "qtsvg", "default-features": false }, { "name": "qtmultimedia", "default-features": false }, { "name": "qgis", "default-features": false } ] }