chore: import upstream snapshot with attribution
CI / Dependency audit (push) Waiting to run
CI / E2E smoke (Playwright) (push) Waiting to run
CI / Validate CITATION.cff (push) Waiting to run
CI / Build and test (push) Waiting to run
Sync labels / sync (push) Failing after 1m9s
Publish Container Image / Build and publish container image (push) Has been cancelled
Deploy Website / Deploy to GitHub Pages (push) Has been cancelled
Deploy Website / Build website and demo (push) Has been cancelled
Deploy viewer / Deploy viewer proxy to Cloudflare Workers (push) Has been cancelled
Deploy tiles / Deploy planetary tile proxy to Cloudflare Workers (push) Has been cancelled
Deploy collab / Deploy collaboration relay to Cloudflare Workers (push) Has been cancelled
@@ -0,0 +1,20 @@
|
||||
.git
|
||||
.github
|
||||
.vscode
|
||||
.idea
|
||||
node_modules
|
||||
**/node_modules
|
||||
apps/geolibre-desktop/dist
|
||||
apps/geolibre-desktop/src-tauri
|
||||
backend/geolibre_server/.venv
|
||||
backend/**/__pycache__
|
||||
workers
|
||||
docs
|
||||
sample-data
|
||||
coverage
|
||||
dist
|
||||
build
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
@@ -0,0 +1,2 @@
|
||||
github: giswqs
|
||||
buy_me_a_coffee: giswqs
|
||||
@@ -0,0 +1,76 @@
|
||||
name: Bug report
|
||||
description: Report unexpected behavior or a possible flaw in GeoLibre.
|
||||
title: "[Bug]: "
|
||||
labels: ["unconfirmed possible issue"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report a problem. Please fill out the sections below so we can reproduce and confirm the issue.
|
||||
|
||||
Reports start as **unconfirmed possible issue**. Once a maintainer reproduces the problem, it is promoted to **bug**.
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Before you start
|
||||
options:
|
||||
- label: I searched the [existing issues](https://github.com/opengeos/GeoLibre/issues) and did not find a duplicate.
|
||||
required: true
|
||||
- label: This is a bug report, not a usage question (questions belong in [Discussions](https://github.com/opengeos/GeoLibre/discussions)).
|
||||
required: true
|
||||
- type: textarea
|
||||
id: what-happened
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: A clear and concise description of the unexpected behavior.
|
||||
placeholder: When I do X, Y happens instead of Z.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Tell us how to reproduce the behavior, step by step.
|
||||
placeholder: |
|
||||
1. Open '...'
|
||||
2. Add data '...'
|
||||
3. Click on '...'
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What did you expect to happen instead?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: app
|
||||
attributes:
|
||||
label: How are you running GeoLibre?
|
||||
description: Web app, desktop app, or Jupyter, plus the version if you know it.
|
||||
placeholder: Desktop app v0.x.y / Web (https://geolibre.app) / Jupyter
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating system and browser
|
||||
description: e.g. Windows 11 + Chrome 126, macOS 14 + Safari, Ubuntu 24.04.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: data
|
||||
attributes:
|
||||
label: Sample data
|
||||
description: If the bug is tied to a specific dataset, share a small sample or a link so we can reproduce it.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: screenshots
|
||||
attributes:
|
||||
label: Screenshots or logs
|
||||
description: Add screenshots, screen recordings, or console output if they help explain the problem.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question or usage help
|
||||
url: https://github.com/opengeos/GeoLibre/discussions/new?category=q-a
|
||||
about: Have a "how do I do X?" question? Please ask in Discussions (Q&A) to keep the issue tracker focused on bugs and feature requests.
|
||||
- name: Ideas and general discussion
|
||||
url: https://github.com/opengeos/GeoLibre/discussions
|
||||
about: Share early-stage ideas, show your work, or chat with the community in Discussions.
|
||||
- name: Documentation
|
||||
url: https://geolibre.app
|
||||
about: Browse the docs and guides before opening an issue.
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Feature request
|
||||
description: Suggest a new feature, enhancement, or UX improvement for GeoLibre.
|
||||
title: "[Feature]: "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for helping make GeoLibre better. Please describe what you would like and why it matters.
|
||||
|
||||
This form labels the issue **enhancement**. The scope you pick below is informational: a maintainer will read your selection and add the matching **enhancement (small)**, **enhancement (medium)**, or **wishlist** label during triage.
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Before you start
|
||||
options:
|
||||
- label: I searched the [existing issues](https://github.com/opengeos/GeoLibre/issues) and did not find a duplicate request.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: What problem does this solve?
|
||||
description: Describe the workflow, pain point, or limitation that motivates this request.
|
||||
placeholder: When working with ..., it is hard to ... because ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: Describe what you would like to see. Mockups or examples are welcome.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: scope
|
||||
attributes:
|
||||
label: Estimated scope
|
||||
description: Your best guess at the size. A maintainer will confirm and apply the matching label.
|
||||
options:
|
||||
- "Small: a minor tweak, small UI change, or text adjustment"
|
||||
- "Medium: a broader feature that is feasible in a reasonable timeframe"
|
||||
- "Wishlist: a larger or longer-term idea"
|
||||
- "Not sure"
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: Any workarounds you currently use, or other approaches you thought about.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,92 @@
|
||||
# Dependabot keeps dependencies and their security advisories current across the
|
||||
# monorepo's four ecosystems. Updates are grouped so a fast-moving repo gets a
|
||||
# few consolidated PRs per week rather than a flood of one-per-dependency PRs.
|
||||
version: 2
|
||||
updates:
|
||||
# JavaScript — one npm workspaces lockfile at the root wires up every JS
|
||||
# workspace, so a single npm entry covers apps/*, packages/*, and workers/*.
|
||||
- package-ecosystem: npm
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
groups:
|
||||
# Security fixes land on their own, immediately actionable PRs.
|
||||
npm-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "*"
|
||||
# Routine minor/patch bumps are batched into one PR to cut review noise.
|
||||
npm-minor-patch:
|
||||
applies-to: version-updates
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
|
||||
# Python — the FastAPI sidecar.
|
||||
- package-ecosystem: pip
|
||||
directory: "/backend/geolibre_server"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
backend-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "*"
|
||||
backend-minor-patch:
|
||||
applies-to: version-updates
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
|
||||
# Python — the `geolibre` Jupyter anywidget package.
|
||||
- package-ecosystem: pip
|
||||
directory: "/python"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
widget-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "*"
|
||||
widget-minor-patch:
|
||||
applies-to: version-updates
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
|
||||
# Rust — the Tauri desktop crate.
|
||||
- package-ecosystem: cargo
|
||||
directory: "/apps/geolibre-desktop/src-tauri"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
cargo-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "*"
|
||||
cargo-minor-patch:
|
||||
applies-to: version-updates
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
|
||||
# GitHub Actions used by the workflows in .github/workflows/.
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
actions-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "*"
|
||||
actions-minor-patch:
|
||||
applies-to: version-updates
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
@@ -0,0 +1,47 @@
|
||||
# Source of truth for this repository's issue/PR labels.
|
||||
# Synced to GitHub by .github/workflows/labels.yml on push to main.
|
||||
# The sync is additive (delete-other-labels is off), so labels not listed
|
||||
# here are left untouched.
|
||||
|
||||
- name: bug
|
||||
color: "d73a4a"
|
||||
description: Something isn't working
|
||||
- name: unconfirmed possible issue
|
||||
color: "fef2c0"
|
||||
description: Reported behavior not yet reproduced or confirmed as a bug
|
||||
- name: enhancement
|
||||
color: "a2eeef"
|
||||
description: New feature or request
|
||||
- name: enhancement (small)
|
||||
color: "c5f0f5"
|
||||
description: Minor feature, small UI tweak, or text adjustment
|
||||
- name: enhancement (medium)
|
||||
color: "76d3dd"
|
||||
description: Broader feature, feasible within a reasonable timeframe
|
||||
- name: wishlist
|
||||
color: "4a0aca"
|
||||
description: Feature requests that require significant efforts
|
||||
- name: question
|
||||
color: "d876e3"
|
||||
description: Further information is requested
|
||||
- name: waiting on user feedback
|
||||
color: "fbca04"
|
||||
description: Progress depends on the reporter (e.g. sample data or verification)
|
||||
- name: documentation
|
||||
color: "0075ca"
|
||||
description: Improvements or additions to documentation
|
||||
- name: duplicate
|
||||
color: "cfd3d7"
|
||||
description: This issue or pull request already exists
|
||||
- name: good first issue
|
||||
color: "7057ff"
|
||||
description: Good for newcomers
|
||||
- name: help wanted
|
||||
color: "008672"
|
||||
description: Extra attention is needed
|
||||
- name: invalid
|
||||
color: "e4e669"
|
||||
description: This doesn't seem right
|
||||
- name: wontfix
|
||||
color: "ffffff"
|
||||
description: This will not be worked on
|
||||
@@ -0,0 +1,201 @@
|
||||
name: Android
|
||||
|
||||
# Android builds run on published releases (matching release.yml), not on every
|
||||
# PR — the Rust cross-compile + Gradle build is slow and rarely needs per-PR
|
||||
# coverage. Use the manual "Run workflow" button (workflow_dispatch) to build a
|
||||
# signed APK on demand from any branch.
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
# Least privilege at the workflow level; the build job below elevates to
|
||||
# contents: write only where it needs to attach release assets.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: android-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
# Keep these in sync with docs/android.md and the local setup.
|
||||
ANDROID_PLATFORM: "platforms;android-34"
|
||||
ANDROID_BUILD_TOOLS: "build-tools;34.0.0"
|
||||
# NDK r27 LTS — Tauri v2's supported line. Bump deliberately.
|
||||
ANDROID_NDK_VERSION: "27.3.13750724"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Android APK (release)
|
||||
runs-on: ubuntu-22.04
|
||||
# Elevated here (not workflow-level) so only this job can write release
|
||||
# assets; workflow_dispatch runs never attach but the grant is harmless.
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
|
||||
- name: Set up Android SDK
|
||||
# Third-party action pinned to a full commit SHA (v3 tag head) per the
|
||||
# repo's policy for non-official actions; a tag could be re-pointed.
|
||||
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3
|
||||
|
||||
- name: Install Android NDK, platform, and build-tools
|
||||
run: |
|
||||
sdkmanager --install \
|
||||
"platform-tools" \
|
||||
"$ANDROID_PLATFORM" \
|
||||
"$ANDROID_BUILD_TOOLS" \
|
||||
"ndk;$ANDROID_NDK_VERSION"
|
||||
echo "NDK_HOME=$ANDROID_SDK_ROOT/ndk/$ANDROID_NDK_VERSION" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install Rust stable with Android targets
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: >-
|
||||
aarch64-linux-android,
|
||||
armv7-linux-androideabi,
|
||||
i686-linux-android,
|
||||
x86_64-linux-android
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
workspaces: apps/geolibre-desktop/src-tauri -> target
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Generate Android project
|
||||
# gen/android is not committed; regenerate it on the clean runner. The
|
||||
# Tauri CLI is resolved through the geolibre-desktop workspace.
|
||||
working-directory: apps/geolibre-desktop
|
||||
run: npx tauri android init
|
||||
|
||||
- name: Apply GeoLibre launcher icons
|
||||
# `tauri android init` writes default Tauri icons; overwrite the generated
|
||||
# mipmaps with the GeoLibre launcher icons checked in under src-tauri/icons.
|
||||
working-directory: apps/geolibre-desktop
|
||||
run: cp -r src-tauri/icons/android/. src-tauri/gen/android/app/src/main/res/
|
||||
|
||||
- name: Build release APKs (per ABI)
|
||||
# Release (not --debug): the size-optimized + stripped Cargo profile keeps
|
||||
# each .so small. --split-per-abi emits one ~40 MB APK per architecture
|
||||
# instead of a single ~150 MB universal APK bundling all four ABIs.
|
||||
# Release APKs are unsigned by default; the next step signs them.
|
||||
working-directory: apps/geolibre-desktop
|
||||
run: npx tauri android build --apk --split-per-abi
|
||||
env:
|
||||
VITE_GEE_OAUTH_CLIENT_ID: ${{ secrets.VITE_GEE_OAUTH_CLIENT_ID }}
|
||||
|
||||
- name: Sign APKs
|
||||
id: sign
|
||||
# With release-keystore secrets set, the APKs are signed for distribution.
|
||||
# Without them, they're signed with a throwaway debug keystore so the CI
|
||||
# artifacts are still installable for testing (do NOT publish those).
|
||||
# Emits signed=release|debug so the release-upload step can refuse to
|
||||
# attach debug-signed APKs to a public GitHub Release.
|
||||
env:
|
||||
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
build_tools="$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -1)"
|
||||
store_pass_file="$RUNNER_TEMP/ks.pass"
|
||||
key_pass_file="$RUNNER_TEMP/key.pass"
|
||||
if [ -n "${KEYSTORE_BASE64:-}" ]; then
|
||||
# Fail fast if the keystore secret is set but its companions are not,
|
||||
# instead of a cryptic apksigner error later.
|
||||
if [ -z "${KEYSTORE_PASSWORD:-}" ] || [ -z "${KEY_ALIAS:-}" ]; then
|
||||
echo "::error::ANDROID_KEYSTORE_PASSWORD and ANDROID_KEY_ALIAS must be set when ANDROID_KEYSTORE_BASE64 is provided"
|
||||
exit 1
|
||||
fi
|
||||
echo "Signing with the release keystore from secrets."
|
||||
echo "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.jks"
|
||||
keystore="$RUNNER_TEMP/release.jks"
|
||||
printf '%s' "$KEYSTORE_PASSWORD" > "$store_pass_file"
|
||||
printf '%s' "${KEY_PASSWORD:-$KEYSTORE_PASSWORD}" > "$key_pass_file"
|
||||
alias="$KEY_ALIAS"
|
||||
sign_mode=release
|
||||
else
|
||||
echo "::warning::No ANDROID_KEYSTORE_BASE64 secret set — signing with a throwaway debug keystore. Installable for testing only, NOT for distribution."
|
||||
sign_mode=debug
|
||||
keystore="$RUNNER_TEMP/debug.jks"
|
||||
"$JAVA_HOME/bin/keytool" -genkeypair -v -keystore "$keystore" \
|
||||
-storepass android -keypass android -alias androiddebugkey \
|
||||
-keyalg RSA -keysize 2048 -validity 10000 \
|
||||
-dname "CN=Android Debug,O=Android,C=US"
|
||||
printf '%s' android > "$store_pass_file"
|
||||
printf '%s' android > "$key_pass_file"
|
||||
alias=androiddebugkey
|
||||
fi
|
||||
out="$RUNNER_TEMP/apks"; mkdir -p "$out"
|
||||
found=0
|
||||
while IFS= read -r unsigned; do
|
||||
found=1
|
||||
# e.g. app-arm64-v8a-release-unsigned.apk -> geolibre-arm64-v8a.apk
|
||||
abi="$(basename "$unsigned" | sed -E 's/^app-(.*)-release-unsigned\.apk$/\1/')"
|
||||
aligned="$RUNNER_TEMP/aligned-$abi.apk"
|
||||
signed="$out/geolibre-android-$abi.apk"
|
||||
"$build_tools/zipalign" -p -f 4 "$unsigned" "$aligned"
|
||||
# Pass passwords via files (pass:file:) so they never appear in the
|
||||
# process argument list / CI logs.
|
||||
"$build_tools/apksigner" sign --ks "$keystore" \
|
||||
--ks-pass "file:$store_pass_file" --key-pass "file:$key_pass_file" \
|
||||
--ks-key-alias "$alias" --out "$signed" "$aligned"
|
||||
"$build_tools/apksigner" verify "$signed"
|
||||
echo "signed $signed ($(du -h "$signed" | cut -f1))"
|
||||
done < <(find apps/geolibre-desktop/src-tauri/gen/android \
|
||||
-name '*release-unsigned.apk')
|
||||
rm -f "$store_pass_file" "$key_pass_file"
|
||||
if [ "$found" -eq 0 ]; then
|
||||
echo "::error::No release-unsigned APKs found"; exit 1
|
||||
fi
|
||||
# Emit the outcome only after every APK has signed and verified, so a
|
||||
# downstream always() step can never read signed=release on a failure.
|
||||
echo "signed=$sign_mode" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload signed APKs
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: geolibre-android-release-apks
|
||||
path: ${{ runner.temp }}/apks/*.apk
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Attach APKs to GitHub Release
|
||||
# Only on a published release, and only when the APKs were signed with the
|
||||
# real release keystore — never publish debug-signed builds as official
|
||||
# downloads. workflow_dispatch runs still get the CI artifact above.
|
||||
if: github.event_name == 'release' && steps.sign.outputs.signed == 'release'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
run: gh release upload "$TAG" "$RUNNER_TEMP"/apks/*.apk --clobber
|
||||
|
||||
- name: Note skipped release upload
|
||||
# Surface why a release run did not attach APKs (missing keystore secrets),
|
||||
# so it does not look like a silent failure.
|
||||
if: github.event_name == 'release' && steps.sign.outputs.signed != 'release'
|
||||
run: |
|
||||
echo "::warning::APKs were debug-signed (no ANDROID_KEYSTORE_BASE64 secret) and were NOT attached to the release. They are available as the CI artifact for testing only."
|
||||
@@ -0,0 +1,203 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
audit:
|
||||
name: Dependency audit
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
# Reads package-lock.json and queries the registry — no install needed, so
|
||||
# this stays fast. Blocking at `high` catches high/critical advisories
|
||||
# without reddening CI for the moderate/low noise Dependabot handles via PR.
|
||||
- name: Audit npm dependencies (high and critical)
|
||||
run: npm audit --audit-level=high
|
||||
|
||||
e2e:
|
||||
name: E2E smoke (Playwright)
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Resolve Playwright version
|
||||
id: pw
|
||||
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cache Playwright browsers
|
||||
id: pw-cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ steps.pw.outputs.version }}
|
||||
|
||||
- name: Install Playwright Chromium (cache miss)
|
||||
if: steps.pw-cache.outputs.cache-hit != 'true'
|
||||
run: npx playwright install --with-deps chromium
|
||||
|
||||
- name: Install Playwright system deps (cache hit)
|
||||
if: steps.pw-cache.outputs.cache-hit == 'true'
|
||||
run: npx playwright install-deps chromium
|
||||
|
||||
- name: Run E2E smoke tests
|
||||
run: npm run test:e2e
|
||||
env:
|
||||
VITE_GEE_OAUTH_CLIENT_ID: ${{ secrets.VITE_GEE_OAUTH_CLIENT_ID }}
|
||||
|
||||
- name: Upload Playwright report
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: |
|
||||
playwright-report/
|
||||
test-results/
|
||||
retention-days: 7
|
||||
|
||||
citation:
|
||||
name: Validate CITATION.cff
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install cffconvert
|
||||
run: python -m pip install cffconvert
|
||||
|
||||
- name: Validate CITATION.cff against the CFF schema
|
||||
run: cffconvert --validate -i CITATION.cff
|
||||
|
||||
- name: Ensure version matches package.json
|
||||
run: |
|
||||
cff_version="$(sed -n 's/^version: *//p' CITATION.cff | tr -d '"' | head -n1)"
|
||||
pkg_version="$(python -c "import json; print(json.load(open('package.json'))['version'])")"
|
||||
echo "CITATION.cff version: $cff_version"
|
||||
echo "package.json version: $pkg_version"
|
||||
if [ "$cff_version" != "$pkg_version" ]; then
|
||||
echo "::error file=CITATION.cff::version ($cff_version) does not match package.json ($pkg_version). Update CITATION.cff version and date-released when bumping the release version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
checks:
|
||||
name: Build and test
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: backend/geolibre_server/pyproject.toml
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Cache Rust dependencies
|
||||
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
|
||||
with:
|
||||
workspaces: apps/geolibre-desktop/src-tauri -> target
|
||||
|
||||
- name: Install Linux desktop dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
curl \
|
||||
file \
|
||||
libayatana-appindicator3-dev \
|
||||
libssl-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libxdo-dev \
|
||||
patchelf \
|
||||
wget
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install backend test dependencies
|
||||
# The full test suite needs the optional engines; without them the
|
||||
# vector/raster/SQL/ML tests skip themselves and CI is green but hollow.
|
||||
run: python -m pip install -e "backend/geolibre_server[test]"
|
||||
|
||||
# Audit the resolved backend environment for known advisories. Non-blocking
|
||||
# (continue-on-error): the sidecar pulls a heavy geospatial/ML dependency
|
||||
# tree whose transitive advisories are often upstream and not immediately
|
||||
# fixable, so this surfaces them without halting the release cadence.
|
||||
# Dependabot opens the fix PRs; this step keeps the signal visible in CI.
|
||||
#
|
||||
# pip-audit runs in a throwaway venv against a frozen snapshot of the test
|
||||
# environment (--exclude-editable drops the local geolibre_server checkout,
|
||||
# which isn't on PyPI). This keeps its own dependency resolution from
|
||||
# mutating the environment the "Run CI gate" step below tests against.
|
||||
- name: Audit Python dependencies (advisory, non-blocking)
|
||||
continue-on-error: true
|
||||
run: |
|
||||
python -m pip freeze --exclude-editable > /tmp/backend-freeze.txt
|
||||
python -m venv /tmp/pip-audit-venv
|
||||
/tmp/pip-audit-venv/bin/pip install --quiet pip-audit
|
||||
/tmp/pip-audit-venv/bin/pip-audit \
|
||||
--progress-spinner off \
|
||||
--requirement /tmp/backend-freeze.txt
|
||||
|
||||
# Drive the documented `npm run ci` gate directly (lint -> build ->
|
||||
# frontend+worker+backend coverage -> rust) so this workflow cannot drift
|
||||
# from the script in package.json.
|
||||
- name: Run CI gate (lint, build, frontend/worker/backend tests, rust)
|
||||
run: npm run ci
|
||||
env:
|
||||
VITE_GEE_OAUTH_CLIENT_ID: ${{ secrets.VITE_GEE_OAUTH_CLIENT_ID }}
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Claude Code Review
|
||||
|
||||
# Uses pull_request_target so the workflow has access to repository secrets
|
||||
# (CLAUDE_CODE_OAUTH_TOKEN) even for PRs opened from forks, which lets Claude
|
||||
# review and comment on ALL PRs. This is safe here because the job only reads
|
||||
# the diff and posts comments. It never installs dependencies or executes the
|
||||
# PR's code, so untrusted fork code is never run with the elevated token.
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
# Optional: Only run on specific file changes
|
||||
# paths:
|
||||
# - "src/**/*.ts"
|
||||
# - "src/**/*.tsx"
|
||||
# - "src/**/*.js"
|
||||
# - "src/**/*.jsx"
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
# Optional: Filter by PR author
|
||||
# if: |
|
||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
||||
# github.event.pull_request.user.login == 'new-developer' ||
|
||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout PR head (read-only; code is reviewed, not executed)
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# pull_request_target defaults to the base ref; explicitly check out
|
||||
# the PR head so Claude reviews the proposed changes.
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code Review
|
||||
id: claude-review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
# Use the workflow's GITHUB_TOKEN for GitHub API calls instead of the
|
||||
# default OIDC -> Claude GitHub App token exchange. That exchange
|
||||
# returns "401 Invalid OIDC token" for OIDC tokens minted in a
|
||||
# pull_request_target context, which broke review on fork PRs. Under
|
||||
# pull_request_target GITHUB_TOKEN already carries the pull-requests
|
||||
# and issues write scopes this job declares, so it can post the review
|
||||
# (as github-actions[bot]). claude_code_oauth_token still authenticates
|
||||
# Claude to the model.
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
prompt: |
|
||||
Perform a thorough code review of pull request ${{ github.repository }}/pull/${{ github.event.pull_request.number }}.
|
||||
|
||||
Inspect the changes with `gh pr diff` and `gh pr view`. When the diff alone is not enough to judge correctness, read the surrounding source files for context.
|
||||
|
||||
Review the changed code for:
|
||||
- Bugs and logic errors, including edge cases, race conditions, and missing error handling
|
||||
- Security issues such as injection, unsafe input handling, and leaked secrets
|
||||
- Performance problems and obvious inefficiencies
|
||||
- Code quality, readability, naming, and maintainability
|
||||
- Adherence to any CLAUDE.md guidelines that apply to the changed files
|
||||
|
||||
Concentrate on the changed lines, but use repository context to judge whether they are correct. Report findings across a range of confidence levels, not only near-certain ones. It is fine to raise a well-reasoned concern even when you are not fully certain, as long as you state your confidence and reasoning. Skip pre-existing issues unrelated to this change.
|
||||
|
||||
Post specific findings as inline review comments on the relevant lines using the create_inline_comment tool. For each comment, briefly explain the issue and, when the fix is small and self-contained, include a committable suggestion block. Group minor nits together rather than posting many separate inline comments.
|
||||
|
||||
After posting inline comments, post exactly one summary comment with `gh pr comment` that starts with the heading "## Code review" and lists the findings grouped by category (Bugs, Security, Performance, Quality, CLAUDE.md), each with a one-line description and confidence. If you genuinely find nothing worth raising, say so and note what you checked.
|
||||
|
||||
Do not approve, merge, or modify any code. Only review and comment. Do not use web fetch; use the gh CLI for all GitHub interactions.
|
||||
claude_args: |
|
||||
--allowedTools "Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr comment:*),Read,Grep,Glob,mcp__github_inline_comment__create_inline_comment"
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
@@ -0,0 +1,102 @@
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
# First-line gate: only proceed on an explicit @claude mention, and resolve
|
||||
# the invoking user's REAL repository permission level before any token is
|
||||
# granted. `author_association == 'COLLABORATOR'` is not sufficient on its
|
||||
# own -- read-only and triage-only collaborators also report COLLABORATOR, so
|
||||
# gating on the enum alone would let a non-push user invoke the agent. This
|
||||
# job runs with a read-only token and emits `trusted=true` only for users who
|
||||
# can already push (write / maintain / admin roles).
|
||||
check-access:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
# This job only calls the REST API (via gh) and never checks out or reads
|
||||
# repository content, so it needs no repository scopes. The
|
||||
# collaborators/permission endpoint requires only metadata:read, which the
|
||||
# GITHUB_TOKEN always retains even with an empty permissions block.
|
||||
permissions: {}
|
||||
outputs:
|
||||
trusted: ${{ steps.check.outputs.trusted }}
|
||||
steps:
|
||||
- name: Resolve actor permission level
|
||||
id: check
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
ACTOR: ${{ github.actor }}
|
||||
run: |
|
||||
if role=$(gh api "repos/${REPO}/collaborators/${ACTOR}/permission" --jq '.role_name' 2>err.log); then
|
||||
echo "Actor ${ACTOR} resolved to role: ${role}"
|
||||
case "${role}" in
|
||||
admin | maintain | write) echo "trusted=true" >> "${GITHUB_OUTPUT}" ;;
|
||||
*) echo "trusted=false" >> "${GITHUB_OUTPUT}" ;;
|
||||
esac
|
||||
elif grep -q 'HTTP 404' err.log; then
|
||||
# 404 = the actor is not a collaborator with any role: not trusted.
|
||||
echo "Actor ${ACTOR} is not a repository collaborator; marking untrusted."
|
||||
echo "trusted=false" >> "${GITHUB_OUTPUT}"
|
||||
else
|
||||
# Any other failure (auth, rate limit, network) must not be silently
|
||||
# downgraded to "untrusted" -- surface it and fail the job instead.
|
||||
echo "::error::Permission lookup for ${ACTOR} failed:"
|
||||
cat err.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
claude:
|
||||
needs: check-access
|
||||
if: needs.check-access.outputs.trusted == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
# Least-privilege token. Each scope maps to a concrete operation the agent
|
||||
# performs, and this job only runs after check-access confirms the invoking
|
||||
# user already has push access. `id-token: write` is intentionally omitted:
|
||||
# this workflow authenticates with an OAuth token and has no OIDC use case.
|
||||
#
|
||||
# Residual risk: this gate controls WHO can invoke the agent, not WHAT it
|
||||
# reads. A trusted maintainer invoking @claude on an issue/PR authored by an
|
||||
# untrusted user still exposes the agent to attacker-controlled text (the
|
||||
# diff or issue body) and thus to prompt injection while it holds a
|
||||
# write-capable token. Review the target before invoking on untrusted input.
|
||||
permissions:
|
||||
contents: write # commit and push changes on a branch when asked to edit code
|
||||
pull-requests: write # open/update PRs and post review replies
|
||||
issues: write # post replies on the triggering issue
|
||||
actions: read # read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
||||
|
||||
# Optional: Add claude_args to customize behavior and configuration
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr *)'
|
||||
@@ -0,0 +1,165 @@
|
||||
name: Cloudflare preview
|
||||
|
||||
# Builds the docs site + web demo for every pull request and uploads it to a
|
||||
# Cloudflare Pages project as a *preview* deployment, then comments the URL on
|
||||
# the PR. This replaces the old Netlify Deploy Previews.
|
||||
#
|
||||
# The build mirrors .github/workflows/pages.yml (the GitHub Pages production
|
||||
# deploy) so previews match what ships:
|
||||
# docs deps -> jupyterlite deps -> npm ci -> web demo build -> docs build ->
|
||||
# bundle demo into site/demo.
|
||||
#
|
||||
# NOTE on fork PRs: the `pull_request` event does NOT expose repository secrets
|
||||
# to PRs opened from forks, so previews only run for branches pushed to this
|
||||
# repo (maintainers and collaborators). External fork PRs are skipped — the job
|
||||
# guard below short-circuits them instead of failing with a missing token.
|
||||
#
|
||||
# Required repository secrets (already present for the Workers deploys):
|
||||
# CLOUDFLARE_API_TOKEN - needs the "Cloudflare Pages: Edit" permission
|
||||
# CLOUDFLARE_ACCOUNT_ID
|
||||
# Optional (baked into the client bundle, same as pages.yml):
|
||||
# VITE_GEE_OAUTH_CLIENT_ID, VITE_PROTOMAPS_API_KEY,
|
||||
# VITE_GOOGLE_MAPS_API_KEY or GOOGLE_MAPS_API_KEY
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: cf-preview-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
preview:
|
||||
name: Build and deploy preview
|
||||
runs-on: ubuntu-latest
|
||||
# Skip fork PRs: secrets are unavailable there, so the deploy would fail.
|
||||
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# Match ci.yml: don't leave the GITHUB_TOKEN in .git/config where an
|
||||
# npm lifecycle script during `npm ci`/`npm run build` could read it.
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
requirements-docs.txt
|
||||
apps/geolibre-desktop/jupyterlite/requirements.txt
|
||||
|
||||
- name: Install documentation dependencies
|
||||
run: python -m pip install -r requirements-docs.txt
|
||||
|
||||
- name: Install JupyterLite build dependencies
|
||||
run: python -m pip install -r apps/geolibre-desktop/jupyterlite/requirements.txt
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build web demo
|
||||
run: npm run build -w geolibre-desktop
|
||||
env:
|
||||
GEOLIBRE_APP_BASE: ./
|
||||
VITE_GEE_OAUTH_CLIENT_ID: ${{ secrets.VITE_GEE_OAUTH_CLIENT_ID }}
|
||||
VITE_PROTOMAPS_API_KEY: ${{ secrets.VITE_PROTOMAPS_API_KEY }}
|
||||
VITE_GOOGLE_MAPS_API_KEY: ${{ secrets.VITE_GOOGLE_MAPS_API_KEY }}
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
VITE_MAPILLARY_ACCESS_TOKEN: ${{ secrets.VITE_MAPILLARY_ACCESS_TOKEN || vars.VITE_MAPILLARY_ACCESS_TOKEN }}
|
||||
VITE_GEOLIBRE_COLLAB_URL: wss://collab.geolibre.app
|
||||
|
||||
- name: Build documentation
|
||||
run: zensical build --strict
|
||||
|
||||
- name: Add web demo to site
|
||||
run: |
|
||||
mkdir -p site/demo
|
||||
cp -R apps/geolibre-desktop/dist/. site/demo/
|
||||
test -f site/demo/index.html
|
||||
test -f site/demo/jupyterlite/lab/index.html
|
||||
|
||||
- name: Offload oversized DuckDB WASM to CDN
|
||||
# Cloudflare Pages rejects any single file > 25 MiB. The bundled DuckDB
|
||||
# WASM modules (duckdb-eh ~35 MiB, duckdb-mvp ~40 MiB) exceed that, so we
|
||||
# drop the local copies and redirect their hashed URLs to jsDelivr at the
|
||||
# exact pinned version (byte-identical to the bundled file). This only
|
||||
# affects the Cloudflare preview; GitHub Pages still serves them locally.
|
||||
# Any OTHER oversized file is a hard error so we never ship a broken site.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
version=$(node -e "console.log(require('./node_modules/@duckdb/duckdb-wasm/package.json').version)")
|
||||
echo "duckdb-wasm version: $version"
|
||||
redirects=site/_redirects
|
||||
touch "$redirects"
|
||||
moved=0
|
||||
# 26214400 bytes = 25 MiB, Cloudflare Pages' exact per-file limit.
|
||||
while IFS= read -r -d '' f; do
|
||||
rel=${f#site}
|
||||
base=$(basename "$f")
|
||||
case "$base" in
|
||||
duckdb-eh-*) cdn=duckdb-eh.wasm ;;
|
||||
duckdb-mvp-*) cdn=duckdb-mvp.wasm ;;
|
||||
duckdb-coi-*) cdn=duckdb-coi.wasm ;;
|
||||
*)
|
||||
echo "::error::Oversized file not handled by CDN redirect: $rel ($(du -h "$f" | cut -f1)). Cloudflare Pages rejects files > 25 MiB."
|
||||
exit 1 ;;
|
||||
esac
|
||||
echo "$rel https://cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm@${version}/dist/${cdn} 302" >> "$redirects"
|
||||
rm "$f"
|
||||
echo "redirect: $rel -> $cdn"
|
||||
moved=$((moved + 1))
|
||||
done < <(find site -type f -size +26214400c -print0)
|
||||
echo "offloaded $moved oversized file(s)"
|
||||
echo "----- site/_redirects -----"
|
||||
cat "$redirects"
|
||||
|
||||
- name: Deploy to Cloudflare Pages
|
||||
id: deploy
|
||||
uses: cloudflare/wrangler-action@v4
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
# --branch is anything other than the project's production branch, so
|
||||
# every run here is a preview deployment with its own URL.
|
||||
command: >-
|
||||
pages deploy site
|
||||
--project-name=geolibre-preview
|
||||
--branch="${{ github.head_ref || github.ref_name }}"
|
||||
--commit-dirty=true
|
||||
|
||||
- name: Comment preview URL on PR
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
DEPLOY_URL: ${{ steps.deploy.outputs.deployment-url }}
|
||||
with:
|
||||
script: |
|
||||
const url = process.env.DEPLOY_URL;
|
||||
if (!url) return;
|
||||
const marker = '<!-- cloudflare-preview -->';
|
||||
const body = `${marker}\n### ⚡ Cloudflare Pages preview\n\n| Item | Value |\n| --- | --- |\n| Preview | ${url} |\n| Demo app | ${url}/demo/ |\n| Commit | \`${context.sha.slice(0, 7)}\` |`;
|
||||
const { owner, repo } = context.repo;
|
||||
const issue_number = context.issue.number;
|
||||
const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number, per_page: 100 });
|
||||
const existing = comments.find((c) => c.body && c.body.includes(marker));
|
||||
if (existing) {
|
||||
await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body });
|
||||
} else {
|
||||
await github.rest.issues.createComment({ owner, repo, issue_number, body });
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Deploy collab
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "workers/collab/**"
|
||||
- ".github/workflows/deploy-collab.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: deploy-collab-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy collaboration relay to Cloudflare Workers
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Deploy to Cloudflare Workers
|
||||
uses: cloudflare/wrangler-action@v4
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
workingDirectory: workers/collab
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Deploy tiles
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "workers/tiles/**"
|
||||
- ".github/workflows/deploy-tiles.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: deploy-tiles-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy planetary tile proxy to Cloudflare Workers
|
||||
runs-on: ubuntu-latest
|
||||
# workflow_dispatch can target any branch; only ever deploy main to
|
||||
# production so a manual run can't publish unmerged code.
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
# Unlike the viewer/collab workers, this one has a real runtime dependency
|
||||
# (upng-js) that wrangler must bundle, so the workspace deps have to be
|
||||
# installed before deploy. wrangler-action does not install for us.
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Deploy to Cloudflare Workers
|
||||
uses: cloudflare/wrangler-action@v4
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
workingDirectory: workers/tiles
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Deploy viewer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "workers/viewer/**"
|
||||
- ".github/workflows/deploy-viewer.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: deploy-viewer-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy viewer proxy to Cloudflare Workers
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Deploy to Cloudflare Workers
|
||||
uses: cloudflare/wrangler-action@v4
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
workingDirectory: workers/viewer
|
||||
@@ -0,0 +1,27 @@
|
||||
name: Sync labels
|
||||
|
||||
# Keeps the repository's labels in sync with .github/labels.yml so they are
|
||||
# version-controlled and recreated automatically if deleted. The sync is
|
||||
# additive: existing labels not listed in the manifest are left untouched.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- .github/labels.yml
|
||||
- .github/workflows/labels.yml
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EndBug/label-sync@v2
|
||||
with:
|
||||
config-file: .github/labels.yml
|
||||
delete-other-labels: false
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Build Store MSIX
|
||||
|
||||
# Manually builds the Windows MSIX package for a Microsoft Store submission and
|
||||
# uploads it as a workflow artifact. This does NOT touch any GitHub release; it
|
||||
# exists so a Store-ready package can be produced on demand (e.g. to fix a
|
||||
# rejected submission) without re-publishing a release.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build MSIX
|
||||
runs-on: windows-2025
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build Tauri release binary
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
VITE_GEE_OAUTH_CLIENT_ID: ${{ secrets.VITE_GEE_OAUTH_CLIENT_ID }}
|
||||
# Strip the in-app "Check for updates" flow from this Store-only build
|
||||
# so the package updates solely through the Store (Microsoft policy
|
||||
# 10.2.5). Set only here — the release.yml installers keep the updater.
|
||||
GEOLIBRE_STORE_BUILD: "1"
|
||||
run: npm run tauri:build -- --no-sign
|
||||
|
||||
- name: Build MSIX package
|
||||
id: build_msix
|
||||
shell: pwsh
|
||||
run: ./packaging/msix/build-msix.ps1
|
||||
|
||||
- name: Upload MSIX artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: geolibre-store-msix
|
||||
path: ${{ steps.build_msix.outputs.msix_path }}
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,105 @@
|
||||
name: Deploy Website
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build website and demo
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: |
|
||||
requirements-docs.txt
|
||||
apps/geolibre-desktop/jupyterlite/requirements.txt
|
||||
|
||||
- name: Install documentation dependencies
|
||||
run: python -m pip install -r requirements-docs.txt
|
||||
|
||||
- name: Install JupyterLite build dependencies
|
||||
# Lets the web build's `prebuild` hook generate the self-hosted
|
||||
# JupyterLite site (served in the Notebook panel); without these the step
|
||||
# skips and the demo's notebook 404s on the published site.
|
||||
run: python -m pip install -r apps/geolibre-desktop/jupyterlite/requirements.txt
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build web demo
|
||||
run: npm run build -w geolibre-desktop
|
||||
env:
|
||||
GEOLIBRE_APP_BASE: ./
|
||||
VITE_GEE_OAUTH_CLIENT_ID: ${{ secrets.VITE_GEE_OAUTH_CLIENT_ID }}
|
||||
# Protomaps API key, baked into the client bundle so the New map
|
||||
# dialog can offer Protomaps basemaps. When unset, those options are
|
||||
# hidden (see getProtomapsStyleUrl in @geolibre/core).
|
||||
VITE_PROTOMAPS_API_KEY: ${{ secrets.VITE_PROTOMAPS_API_KEY }}
|
||||
# Google Photorealistic 3D Tiles and Google Street View use the same
|
||||
# Maps key. Prefer the Vite-prefixed secret name, but also pass the
|
||||
# bare name below so vite.config.ts can expose local/CI test setups.
|
||||
VITE_GOOGLE_MAPS_API_KEY: ${{ secrets.VITE_GOOGLE_MAPS_API_KEY }}
|
||||
GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }}
|
||||
# Mapillary access token, baked into the client bundle so the
|
||||
# Mapillary/Street View plugins work without each visitor pasting a
|
||||
# token. It ends up publicly readable in the served JS, so it is not a
|
||||
# true secret; a repo variable works as well as a secret here.
|
||||
VITE_MAPILLARY_ACCESS_TOKEN: ${{ secrets.VITE_MAPILLARY_ACCESS_TOKEN || vars.VITE_MAPILLARY_ACCESS_TOKEN }}
|
||||
# Public relay URL (baked into the client bundle), not a secret. Lights
|
||||
# up live collaboration; requires the workers/collab relay to be
|
||||
# deployed (see docs/collaboration.md).
|
||||
VITE_GEOLIBRE_COLLAB_URL: wss://collab.geolibre.app
|
||||
|
||||
- name: Build documentation
|
||||
run: zensical build --strict
|
||||
|
||||
- name: Add web demo to site
|
||||
run: |
|
||||
mkdir -p site/demo
|
||||
cp -R apps/geolibre-desktop/dist/. site/demo/
|
||||
test -f site/demo/index.html
|
||||
test -f site/demo/jupyterlite/lab/index.html
|
||||
|
||||
- name: Upload Pages artifact
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: site
|
||||
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages
|
||||
if: github.event_name != 'pull_request'
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
@@ -0,0 +1,70 @@
|
||||
name: Publish Container Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
concurrency:
|
||||
group: container-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and publish container image
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set image name
|
||||
id: image
|
||||
run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up QEMU
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/setup-qemu-action@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract Docker metadata
|
||||
id: metadata
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ steps.image.outputs.name }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha,prefix=sha-
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and publish image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
build-args: |
|
||||
VITE_GEE_OAUTH_CLIENT_ID=${{ secrets.VITE_GEE_OAUTH_CLIENT_ID }}
|
||||
VITE_MAPILLARY_ACCESS_TOKEN=${{ secrets.VITE_MAPILLARY_ACCESS_TOKEN || vars.VITE_MAPILLARY_ACCESS_TOKEN }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
@@ -0,0 +1,99 @@
|
||||
name: Publish Python package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "python/**"
|
||||
- "apps/geolibre-desktop/src/**"
|
||||
- "packages/**"
|
||||
- "scripts/build-embed.mjs"
|
||||
- ".github/workflows/publish-python.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: publish-python-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test Python package
|
||||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
# Cover the declared minimum (requires-python >= 3.11) and current
|
||||
# releases so newer-only syntax can't slip in unnoticed.
|
||||
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
# Tests cover the pure-Python builders and do not need the bundled web
|
||||
# app, so install only the runtime/test deps and run against the sources.
|
||||
- name: Install test dependencies
|
||||
run: python -m pip install anywidget traitlets pytest
|
||||
|
||||
- name: Run tests
|
||||
run: python -m pytest python/tests
|
||||
env:
|
||||
PYTHONPATH: python/src
|
||||
|
||||
publish:
|
||||
name: Build wheel and publish
|
||||
runs-on: ubuntu-22.04
|
||||
needs: test
|
||||
if: github.event_name == 'release'
|
||||
environment: pypi
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # PyPI Trusted Publishing (OIDC)
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install build backend
|
||||
run: python -m pip install build
|
||||
|
||||
- name: Build wheel and sdist
|
||||
working-directory: python
|
||||
run: python -m build
|
||||
env:
|
||||
# Force a fresh embed build so the published wheel always carries the
|
||||
# current app bundle.
|
||||
GEOLIBRE_FORCE_JS_BUILD: "1"
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
VITE_GEE_OAUTH_CLIENT_ID: ${{ secrets.VITE_GEE_OAUTH_CLIENT_ID }}
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
|
||||
with:
|
||||
packages-dir: python/dist
|
||||
@@ -0,0 +1,361 @@
|
||||
name: Release Desktop App
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.event.release.tag_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux x64
|
||||
os: ubuntu-22.04
|
||||
args: --bundles deb,rpm,appimage
|
||||
rust-targets: ""
|
||||
|
||||
- name: Windows x64
|
||||
os: windows-2025
|
||||
args: --no-sign
|
||||
rust-targets: ""
|
||||
|
||||
# macOS bundles are Developer ID signed and notarized via the
|
||||
# APPLE_* secrets wired into the tauri-action step below, so users can
|
||||
# open the DMG without stripping the quarantine attribute. Do not pass
|
||||
# --no-sign here.
|
||||
- name: macOS Apple Silicon
|
||||
os: macos-latest
|
||||
args: --target aarch64-apple-darwin
|
||||
rust-targets: aarch64-apple-darwin
|
||||
|
||||
- name: macOS Intel
|
||||
os: macos-latest
|
||||
args: --target x86_64-apple-darwin
|
||||
rust-targets: x86_64-apple-darwin
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
cache-dependency-path: package-lock.json
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install macOS Rust target
|
||||
if: matrix.rust-targets != ''
|
||||
run: rustup target add ${{ matrix.rust-targets }}
|
||||
|
||||
- name: Install Linux dependencies
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential \
|
||||
curl \
|
||||
file \
|
||||
libayatana-appindicator3-dev \
|
||||
libfuse2 \
|
||||
librsvg2-dev \
|
||||
libssl-dev \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libxdo-dev \
|
||||
patchelf \
|
||||
rpm \
|
||||
wget
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build and upload release assets
|
||||
uses: tauri-apps/tauri-action@v0.6
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
VITE_GEE_OAUTH_CLIENT_ID: ${{ secrets.VITE_GEE_OAUTH_CLIENT_ID }}
|
||||
# macOS Developer ID signing + notarization. The secrets only resolve
|
||||
# to real values on macOS runners; Linux/Windows get empty strings so
|
||||
# the credentials are never placed in their process environment.
|
||||
# APPLE_SIGNING_IDENTITY defaults to "-" when the secret is absent
|
||||
# (e.g. a fork without secrets) so macOS still ad-hoc signs rather
|
||||
# than failing the build now that tauri.conf.json no longer pins it.
|
||||
APPLE_CERTIFICATE: ${{ runner.os == 'macOS' && secrets.APPLE_CERTIFICATE || '' }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ runner.os == 'macOS' && secrets.APPLE_CERTIFICATE_PASSWORD || '' }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ runner.os == 'macOS' && (secrets.APPLE_SIGNING_IDENTITY || '-') || '' }}
|
||||
APPLE_ID: ${{ runner.os == 'macOS' && secrets.APPLE_ID || '' }}
|
||||
APPLE_PASSWORD: ${{ runner.os == 'macOS' && secrets.APPLE_PASSWORD || '' }}
|
||||
APPLE_TEAM_ID: ${{ runner.os == 'macOS' && secrets.APPLE_TEAM_ID || '' }}
|
||||
with:
|
||||
projectPath: apps/geolibre-desktop
|
||||
releaseId: ${{ github.event.release.id }}
|
||||
tagName: ${{ github.event.release.tag_name }}
|
||||
args: ${{ matrix.args }}
|
||||
uploadPlainBinary: true
|
||||
|
||||
- name: Build MSIX package
|
||||
id: build_msix
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: ./packaging/msix/build-msix.ps1
|
||||
|
||||
- name: Upload MSIX release asset
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
$msix = "${{ steps.build_msix.outputs.msix_path }}"
|
||||
if (-not (Test-Path $msix)) {
|
||||
throw "No MSIX package was created at '$msix'."
|
||||
}
|
||||
gh release upload "${{ github.event.release.tag_name }}" "$msix" --clobber
|
||||
|
||||
- name: Build portable zip
|
||||
id: build_portable
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: ./packaging/portable/build-portable.ps1
|
||||
|
||||
- name: Upload portable zip release asset
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
$portable = "${{ steps.build_portable.outputs.portable_path }}"
|
||||
if (-not (Test-Path $portable)) {
|
||||
throw "No portable zip was created at '$portable'."
|
||||
}
|
||||
gh release upload "${{ github.event.release.tag_name }}" "$portable" --clobber
|
||||
|
||||
homebrew:
|
||||
name: Update Homebrew cask
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
# The primary cask is in the official homebrew/homebrew-cask repo, which
|
||||
# BrewTestBot auto-bumps via livecheck. This job only updates the self-hosted
|
||||
# tap (opengeos/homebrew-geolibre), kept as a fallback.
|
||||
# Serialise tap pushes so two close releases can't race on a non-fast-forward.
|
||||
concurrency:
|
||||
group: homebrew-tap-update
|
||||
cancel-in-progress: false
|
||||
# Only update the tap for full releases, not prereleases.
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Compute DMG checksums
|
||||
id: checksums
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
version="${TAG#v}"
|
||||
arm="GeoLibre.Desktop_${version}_aarch64.dmg"
|
||||
intel="GeoLibre.Desktop_${version}_x64.dmg"
|
||||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "$arm" --pattern "$intel" --dir dmgs
|
||||
{
|
||||
echo "version=$version"
|
||||
echo "sha_arm=$(sha256sum "dmgs/$arm" | awk '{print $1}')"
|
||||
echo "sha_intel=$(sha256sum "dmgs/$intel" | awk '{print $1}')"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check out Homebrew tap
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: opengeos/homebrew-geolibre
|
||||
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
path: tap
|
||||
persist-credentials: false
|
||||
|
||||
- name: Render and commit cask
|
||||
env:
|
||||
VERSION: ${{ steps.checksums.outputs.version }}
|
||||
SHA256_ARM: ${{ steps.checksums.outputs.sha_arm }}
|
||||
SHA256_INTEL: ${{ steps.checksums.outputs.sha_intel }}
|
||||
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
run: |
|
||||
mkdir -p tap/Casks
|
||||
scripts/render-homebrew-cask.sh > tap/Casks/geolibre.rb
|
||||
cd tap
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git add Casks/geolibre.rb
|
||||
# --cached so an initially untracked cask (fresh tap) is still detected.
|
||||
if git diff --cached --quiet; then
|
||||
echo "Cask already up to date for ${VERSION}."
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "geolibre ${VERSION}"
|
||||
git push "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/opengeos/homebrew-geolibre.git" HEAD:main
|
||||
|
||||
aur:
|
||||
name: Publish AUR package
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
# Fully isolated from the asset build and the Homebrew job: it only `needs`
|
||||
# build for the .deb, nothing depends on it, and continue-on-error means a
|
||||
# failed AUR push leaves the release (and its other publish targets) green.
|
||||
continue-on-error: true
|
||||
# Serialise AUR pushes so two close releases can't race on a non-fast-forward.
|
||||
concurrency:
|
||||
group: aur-publish
|
||||
cancel-in-progress: false
|
||||
# Only publish for full releases, not prereleases.
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
env:
|
||||
# Surfaced as an env var so steps can skip cleanly when the secret is
|
||||
# absent (forks, or before the maintainer configures it).
|
||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Render PKGBUILD from the release .deb
|
||||
if: ${{ env.AUR_SSH_PRIVATE_KEY != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
version="${TAG#v}"
|
||||
deb="GeoLibre.Desktop_${version}_amd64.deb"
|
||||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern "$deb" --dir debs
|
||||
sha="$(sha256sum "debs/$deb" | awk '{print $1}')"
|
||||
mkdir -p aur
|
||||
VERSION="$version" SHA256_AMD64="$sha" REPO="$GITHUB_REPOSITORY" \
|
||||
scripts/render-aur-pkgbuild.sh > aur/PKGBUILD
|
||||
echo "----- rendered PKGBUILD -----"
|
||||
cat aur/PKGBUILD
|
||||
|
||||
- name: Publish to the AUR
|
||||
if: ${{ env.AUR_SSH_PRIVATE_KEY != '' }}
|
||||
# Pinned to a commit SHA (not a mutable tag) because this step receives
|
||||
# the AUR SSH private key. Update deliberately when bumping the version.
|
||||
uses: KSXGitHub/github-actions-deploy-aur@da03e160361ce01bf087e790b6ffd196d7dccff7 # v4.1.3
|
||||
with:
|
||||
pkgname: geolibre-bin
|
||||
pkgbuild: aur/PKGBUILD
|
||||
commit_username: github-actions[bot]
|
||||
commit_email: github-actions[bot]@users.noreply.github.com
|
||||
ssh_private_key: ${{ env.AUR_SSH_PRIVATE_KEY }}
|
||||
commit_message: "Update to ${{ github.event.release.tag_name }}"
|
||||
allow_empty_commits: false
|
||||
updpkgsums: false
|
||||
|
||||
copr:
|
||||
name: Publish Fedora COPR package
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
# Same isolation as the AUR job: only needs build for the .rpm, nothing
|
||||
# depends on it, continue-on-error keeps a COPR failure from reddening the
|
||||
# release, and it skips itself when the API token is absent (forks).
|
||||
continue-on-error: true
|
||||
concurrency:
|
||||
group: copr-publish
|
||||
cancel-in-progress: false
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
env:
|
||||
COPR_API_TOKEN: ${{ secrets.COPR_API_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build SRPM and submit to COPR
|
||||
if: ${{ env.COPR_API_TOKEN != '' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
COPR_PROJECT: giswqs/geolibre
|
||||
run: |
|
||||
version="${TAG#v}"
|
||||
# Release date (YYYY-MM-DD) for the metainfo <release> and %changelog.
|
||||
release_date="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--json publishedAt --jq '.publishedAt' | cut -dT -f1)"
|
||||
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y -qq rpm appstream
|
||||
pipx install copr-cli
|
||||
# copr-cli imports `rich` but does not always declare it as a dependency.
|
||||
pipx inject copr-cli rich
|
||||
|
||||
# COPR API token config from the secret. Create it with restrictive
|
||||
# permissions atomically (umask), not chmod-after-write.
|
||||
mkdir -p ~/.config
|
||||
(umask 177; printf '%s\n' "$COPR_API_TOKEN" > ~/.config/copr)
|
||||
|
||||
mkdir -p ~/rpmbuild/SPECS ~/rpmbuild/SOURCES
|
||||
VERSION="$version" DATE="$release_date" \
|
||||
scripts/render-linux-metainfo.sh > ~/rpmbuild/SOURCES/org.geolibre.desktop.metainfo.xml
|
||||
VERSION="$version" DATE="$release_date" REPO="$GITHUB_REPOSITORY" \
|
||||
scripts/render-copr-spec.sh > ~/rpmbuild/SPECS/geolibre.spec
|
||||
# The spec ships the project license as Source2.
|
||||
cp LICENSE ~/rpmbuild/SOURCES/
|
||||
|
||||
# Validate the metainfo in CI, independently of COPR's mock build.
|
||||
appstreamcli validate --no-net ~/rpmbuild/SOURCES/org.geolibre.desktop.metainfo.xml
|
||||
|
||||
# The upstream RPM is Source0; rpmbuild needs it locally to build the
|
||||
# SRPM (it does not fetch URLs).
|
||||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
|
||||
--pattern "GeoLibre.Desktop-${version}-1.x86_64.rpm" --dir ~/rpmbuild/SOURCES
|
||||
# Log the checksum of the repackaged RPM for auditability.
|
||||
sha256sum ~/rpmbuild/SOURCES/"GeoLibre.Desktop-${version}-1.x86_64.rpm" \
|
||||
| tee -a "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
srpm="$(rpmbuild -bs ~/rpmbuild/SPECS/geolibre.spec \
|
||||
--define "_topdir $HOME/rpmbuild" | sed -n 's/^Wrote: //p')"
|
||||
[[ -n "$srpm" ]] || { echo "::error::rpmbuild produced no SRPM path"; exit 1; }
|
||||
echo "Built SRPM: $srpm"
|
||||
copr-cli build --nowait "$COPR_PROJECT" "$srpm"
|
||||
|
||||
winget:
|
||||
name: Publish to winget
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
# Isolated like the AUR/COPR jobs: only needs build for the release assets,
|
||||
# nothing depends on it, continue-on-error keeps a winget hiccup from
|
||||
# failing the release, and it skips itself when WINGET_TOKEN is unset (forks,
|
||||
# or before the maintainer configures it).
|
||||
continue-on-error: true
|
||||
concurrency:
|
||||
group: winget-publish
|
||||
cancel-in-progress: false
|
||||
# Only publish for full releases, not prereleases.
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
env:
|
||||
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
|
||||
steps:
|
||||
- name: Submit OpenGeos.GeoLibre to winget-pkgs
|
||||
if: ${{ env.WINGET_TOKEN != '' }}
|
||||
# Pinned to a commit SHA (not the mutable v2 tag) because this step
|
||||
# receives a token with access to the winget-pkgs fork. winget-releaser
|
||||
# derives the version from the release tag and uses komac under the hood.
|
||||
uses: vedantmgoyal9/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
|
||||
with:
|
||||
identifier: OpenGeos.GeoLibre
|
||||
installers-regex: '_x64-setup\.exe$'
|
||||
fork-user: giswqs
|
||||
token: ${{ env.WINGET_TOKEN }}
|
||||
@@ -0,0 +1,65 @@
|
||||
node_modules/
|
||||
dist/
|
||||
dist-embed/
|
||||
# Generated by `npm run build:jupyterlite` (best-effort prebuild); not committed.
|
||||
apps/geolibre-desktop/public/jupyterlite/
|
||||
# JupyterLite (doit) build cache, written to the working dir during the build.
|
||||
.jupyterlite.doit.db
|
||||
# The kernel-side geolibre client and the Welcome example are staged here from
|
||||
# their canonical backend copies by scripts/build-jupyterlite.mjs.
|
||||
apps/geolibre-desktop/jupyterlite/files/geolibre.py
|
||||
apps/geolibre-desktop/jupyterlite/files/Welcome.ipynb
|
||||
site/
|
||||
target/
|
||||
.tauri/
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Generated by the copy-vector-ops Vite plugin from the backend source of truth
|
||||
# (backend/geolibre_server/geolibre_server/vector_ops.py).
|
||||
apps/geolibre-desktop/src/lib/pyodide/vector_ops.generated.py
|
||||
|
||||
# Generated by the copy-rtl-text Vite plugin from @mapbox/mapbox-gl-rtl-text.
|
||||
apps/geolibre-desktop/src/lib/vendor/mapbox-gl-rtl-text.generated.js
|
||||
|
||||
# Generated by the copy-cesium-assets Vite plugin from node_modules/cesium.
|
||||
apps/geolibre-desktop/public/cesium/
|
||||
.venv/
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
# Coverage artifacts (pytest-cov data file, HTML/XML reports, c8 output)
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage/
|
||||
htmlcov/
|
||||
coverage.xml
|
||||
.idea/
|
||||
.vscode/
|
||||
*.tsbuildinfo
|
||||
**/src-tauri/gen/
|
||||
.playwright-cli/
|
||||
.claude/
|
||||
uv.lock
|
||||
|
||||
# Playwright E2E artifacts
|
||||
test-results/
|
||||
playwright-report/
|
||||
blob-report/
|
||||
.playwright/
|
||||
private/
|
||||
|
||||
# Documentation screenshots are hosted on Cloudflare R2 (data.geolibre.app/images),
|
||||
# not committed to the repo, to save space.
|
||||
docs/assets/screenshots/
|
||||
python/examples/my-map.geolibre.json
|
||||
|
||||
# Cloudflare Workers local dev state (wrangler dev / miniflare)
|
||||
.wrangler/
|
||||
|
||||
# Rendered Homebrew cask, emitted to the working dir by scripts/render-*-cask.sh
|
||||
# for submission to a tap / homebrew-cask; not committed to this repo.
|
||||
/geolibre.rb
|
||||
@@ -0,0 +1,40 @@
|
||||
ci:
|
||||
skip: [npm-build, eslint]
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: check-json
|
||||
# This E2E fixture is intentionally malformed JSON — it drives the
|
||||
# error-handling spec that asserts the app rejects a broken drop.
|
||||
exclude: ^e2e/fixtures/malformed\.geojson$
|
||||
- id: check-added-large-files
|
||||
args: ["--maxkb=1024", "--enforce-all"]
|
||||
- id: check-merge-conflict
|
||||
- id: check-toml
|
||||
- id: check-yaml
|
||||
args: ["--unsafe"]
|
||||
- id: end-of-file-fixer
|
||||
- id: mixed-line-ending
|
||||
args: ["--fix=lf"]
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: eslint
|
||||
name: eslint
|
||||
entry: npm run lint
|
||||
language: system
|
||||
pass_filenames: false
|
||||
types_or: [ts, tsx, javascript, jsx]
|
||||
- id: npm-build
|
||||
name: npm build
|
||||
entry: npm run build
|
||||
language: system
|
||||
pass_filenames: false
|
||||
|
||||
- repo: https://github.com/kynan/nbstripout
|
||||
rev: 0.9.1
|
||||
hooks:
|
||||
- id: nbstripout
|
||||
@@ -0,0 +1,24 @@
|
||||
cff-version: 1.2.0
|
||||
title: GeoLibre
|
||||
message: "If you use this software, please cite it using the metadata from this file."
|
||||
type: software
|
||||
abstract: "GeoLibre is a free and open-source, lightweight, cloud-native GIS platform for visualizing, exploring, and analyzing geospatial data across web browsers, desktop applications, mobile devices, and Jupyter notebooks while keeping data local and private."
|
||||
authors:
|
||||
- given-names: Qiusheng
|
||||
family-names: Wu
|
||||
email: giswqs@gmail.com
|
||||
affiliation: "University of Tennessee, Knoxville, USA"
|
||||
orcid: "https://orcid.org/0000-0001-5437-4073"
|
||||
version: 2.0.0
|
||||
date-released: 2026-07-10
|
||||
doi: 10.5281/zenodo.20785400
|
||||
repository-code: "https://github.com/opengeos/GeoLibre"
|
||||
url: "https://geolibre.app"
|
||||
license: MIT
|
||||
keywords:
|
||||
- GIS
|
||||
- geospatial
|
||||
- remote sensing
|
||||
- data visualization
|
||||
- MapLibre
|
||||
- Python
|
||||
@@ -0,0 +1,94 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Repository shape
|
||||
|
||||
GeoLibre is a single **npm workspaces monorepo** (`apps/*`, `packages/*`, `workers/*`) plus two non-npm components: a Python FastAPI sidecar (`backend/geolibre_server`) and a separate Python package (`python/`, the `geolibre` Jupyter anywidget). One `npm install` at the root wires up every JS workspace. Use **npm** (the repo tracks `package-lock.json`), Node **22+**.
|
||||
|
||||
The same React app ships three ways: native desktop via **Tauri v2** (`apps/geolibre-desktop/src-tauri`), a browser web build served by nginx (Docker), and embedded in Jupyter (the `python/` package bundles a build of the web app into its wheel).
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
npm run dev # web dev server → http://localhost:5173
|
||||
npm run tauri:dev # desktop app (required for filesystem dialogs, local MBTiles, local raster reads)
|
||||
npm run build # production web build → apps/geolibre-desktop/dist/
|
||||
npm run tauri:build # desktop installers → apps/geolibre-desktop/src-tauri/target/release/bundle/
|
||||
npm run typecheck # alias for the full build (tsc -b && vite build) — writes to dist/, not a pure type-check
|
||||
npm run ci # full local gate: build + frontend + worker + backend + rust check
|
||||
```
|
||||
|
||||
Tests:
|
||||
|
||||
```bash
|
||||
npm run test:frontend # node --test over tests/*.test.ts (tsx loader)
|
||||
npm run test:frontend:coverage # same, plus a per-file coverage summary (Node built-in)
|
||||
node --import tsx --test tests/<name>.test.ts # a single frontend test file
|
||||
npm run test:backend # pytest backend/geolibre_server/tests
|
||||
npm run test:backend:coverage # same, plus a pytest-cov term-missing report
|
||||
python -m pytest backend/geolibre_server/tests/test_x.py::test_y # a single backend test
|
||||
npm run test:worker # typecheck workers/viewer
|
||||
npm run test:e2e # Playwright smoke tests (e2e/) against the built web app
|
||||
npm run check:rust # cargo check the Tauri crate
|
||||
```
|
||||
|
||||
The `:coverage` variants run the same suites and print a coverage summary; CI
|
||||
runs them so every build reports coverage. They are now **gated on a floor**:
|
||||
`test:frontend:coverage` fails below 78% lines / 78% branches / 63% functions,
|
||||
and `test:backend:coverage` fails below 55% (`--cov-fail-under`). The floors sit
|
||||
a few points under the current numbers as a **ratchet** — regressions fail CI,
|
||||
and when coverage rises comfortably above a floor, raise the floor to lock in the
|
||||
gain. The frontend
|
||||
report only counts files a test actually imports, so a module with no test does
|
||||
not appear at all rather than as 0%. The backend coverage run (and `npm run ci`,
|
||||
which calls the `:coverage` variants) needs `pytest-cov` from the backend `dev`
|
||||
extra. Install the **`test`** extra to run the *full* backend suite — without
|
||||
the optional engines (geopandas/rasterio/sedona/httpx) the vector/raster/SQL/ML
|
||||
tests skip themselves and CI is green but hollow:
|
||||
`pip install -e "backend/geolibre_server[test]"`.
|
||||
|
||||
`npm run test:e2e` builds the web app, serves it with `vite preview`, and drives
|
||||
it with Playwright (`@playwright/test`). First run: `npx playwright install
|
||||
chromium`. The webServer reuses an already-running preview locally and rebuilds
|
||||
in CI; add specs under `e2e/`.
|
||||
|
||||
Dependencies are watched two ways: **Dependabot** (`.github/dependabot.yml`)
|
||||
opens grouped weekly update PRs for npm, pip (backend + `python/`), cargo, and
|
||||
Actions, and the CI **`audit` job** runs `npm audit --audit-level=high`
|
||||
(blocking) plus a non-blocking `pip-audit` of the resolved backend environment.
|
||||
|
||||
The `python/` package has its own pytest suite (`cd python && pytest`) and is built into a wheel via `npm run build:embed` (produces `apps/geolibre-desktop/dist-embed`, consumed by `python/hatch_build.py`). Its version is dynamic, sourced from `python/src/geolibre/__init__.py`.
|
||||
|
||||
## Pre-commit
|
||||
|
||||
`.pre-commit-config.yaml` includes a **local `npm-build` hook**, so `pre-commit run` compiles the whole app — it is slow and can touch unrelated build state. Scope it to the files you changed: `pre-commit run --files <paths>`. Run it before pushing.
|
||||
|
||||
## Architecture (the parts that span files)
|
||||
|
||||
The app is **store-driven**. `@geolibre/core` holds the Zustand store, domain types, and the `.geolibre.json` project schema — it is the single source of truth. Data flows one way:
|
||||
|
||||
1. Data enters through the Add Data menus, Tauri dialogs, the browser file picker, drag-and-drop, or a plugin control.
|
||||
2. Local vector files that MapLibre can't render directly are converted to GeoJSON in-browser by **DuckDB-WASM Spatial** (`INSTALL spatial; LOAD spatial;` → `ST_Read`; GeoParquet via the Parquet reader; zipped Shapefiles via `shpjs` with a DuckDB fallback; KMZ unzipped client-side). The result calls `addGeoJsonLayer`.
|
||||
3. Tile/service/raster/ArcGIS/MBTiles/plugin layers become `GeoLibreLayer` records.
|
||||
4. `MapCanvas` subscribes to `layers`; `MapController.syncLayers` (`@geolibre/map`) reconciles MapLibre sources/layers and the layer control. **You don't mutate MapLibre directly from UI** — you change store state and let sync apply it.
|
||||
|
||||
Rendering is MapLibre GL JS in the webview, with **deck.gl** for raster/point-cloud/3D overlays.
|
||||
|
||||
**Packages:** `@geolibre/core` (types, project format, store) · `@geolibre/map` (MapLibre lifecycle + layer sync) · `@geolibre/ui` (shadcn-style primitives) · `@geolibre/processing` (client-side algorithm registry) · `@geolibre/plugins` (plugin interface + built-in plugins) · `geolibre-desktop` (shell layout, Tauri I/O, composition).
|
||||
|
||||
**Plugins:** Built-in plugins live in `packages/plugins/src/plugins/`, are exported from that package's `index.ts`, and registered in `apps/geolibre-desktop/src/hooks/usePlugins.ts`. External plugins load from zips or a `plugin.json` manifest; bundled drop-ins under `apps/geolibre-desktop/public/plugins/<id>/` bake into both web and desktop builds. See `docs/plugin-api.md`.
|
||||
|
||||
**Python sidecar** (`backend/geolibre_server`, FastAPI on `127.0.0.1:8765`): backs the Whitebox toolbox, format Conversion tools, and Raster tools (rasterio). The desktop app starts it on demand. It is **optional** — Vector tools (Processing → Vector) run client-side with Turf.js and only use the sidecar's `/vector` endpoints (GeoPandas/Shapely) when the optional `vector` extra is installed; the dialog falls back to the client engine via `/vector/status`. Optional extras: `conversion`, `vector`, `raster`. Some conversions (PMTiles, Whitebox) are amd64-only.
|
||||
|
||||
The browser build proxies the sidecar at `/sidecar` (same-origin, no CORS); confined to `GEOLIBRE_CONVERSION_ROOTS` (default `/data`). Local MBTiles use a custom MapLibre protocol backed by Tauri commands.
|
||||
|
||||
## Conventions
|
||||
|
||||
- Never commit directly to `main`; branch and open a PR.
|
||||
- Tauri CSP allowlists tile/style hosts (OpenFreeMap, CARTO) — new external map/tile hosts must be added there.
|
||||
- Map/tile-host CORS for selected release assets is handled by a dev-server raster proxy.
|
||||
- For MapLibre control styling fixes, add scoped overrides in `apps/geolibre-desktop/src/index.css`, never edit `node_modules`.
|
||||
- The Processing **menu** (`ProcessingMenu.tsx`) renders from a checked-in, auto-generated catalog, `apps/geolibre-desktop/src/lib/whitebox-menu-catalog.ts` (do not hand-edit). Whenever `geolibre-wasm` is bumped (in `packages/processing/package.json`) — including Dependabot PRs — run `node scripts/gen-whitebox-menu-catalog.mjs` and commit the result, or new/renamed WASM tools silently miss the menu (the Processing **dialog** lists them dynamically, so the gap only shows in the menu).
|
||||
- UI strings are translatable via **react-i18next**; catalogs live in `apps/geolibre-desktop/src/i18n/locales/*.json` (`en.json` is the source of truth, typed by `i18next.d.ts`). Use `t()` for new user-facing strings; a `?locale`/`?lang` query param sets the embed language. See `docs/i18n.md`.
|
||||
- Reference docs: `docs/architecture.md`, `docs/project-format.md`, `docs/plugin-api.md`, `docs/python.md`, `docs/i18n.md`, `docs/contributing.md`.
|
||||
@@ -0,0 +1,28 @@
|
||||
# Contributing to GeoLibre
|
||||
|
||||
Thanks for your interest in improving GeoLibre. The full contributing guide,
|
||||
including development setup, the repository layout, the quality gate, and the
|
||||
pull request workflow, lives in the documentation:
|
||||
|
||||
**<https://geolibre.app/contributing/>** (source: [`docs/contributing.md`](docs/contributing.md))
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/opengeos/GeoLibre.git
|
||||
cd GeoLibre
|
||||
npm install
|
||||
npm run dev # web build at http://localhost:5173
|
||||
```
|
||||
|
||||
Before opening a pull request:
|
||||
|
||||
```bash
|
||||
pre-commit run --all-files
|
||||
npm run ci
|
||||
```
|
||||
|
||||
Branch off `main` (never commit to it directly), keep changes focused, follow
|
||||
[Conventional Commits](https://www.conventionalcommits.org/) for messages, and
|
||||
open your pull request against `main`. Found a bug or have an idea? Open an
|
||||
[issue](https://github.com/opengeos/GeoLibre/issues).
|
||||
@@ -0,0 +1,101 @@
|
||||
# Run the build stage on the builder's native platform: the output is
|
||||
# arch-independent static files, so emulating arm64 with QEMU here only
|
||||
# slows down multi-arch builds without changing the result.
|
||||
# ($BUILDPLATFORM is a Docker-provided automatic ARG, set by BuildKit.)
|
||||
FROM --platform=$BUILDPLATFORM node:22-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy every workspace member's package.json before npm ci so the install
|
||||
# layer is cached. Adding a new package under apps/ or packages/ requires
|
||||
# adding its package.json here, or npm ci fails with a missing workspace.
|
||||
COPY package.json package-lock.json ./
|
||||
COPY apps/geolibre-desktop/package.json apps/geolibre-desktop/package.json
|
||||
COPY packages/core/package.json packages/core/package.json
|
||||
COPY packages/map/package.json packages/map/package.json
|
||||
COPY packages/plugins/package.json packages/plugins/package.json
|
||||
COPY packages/processing/package.json packages/processing/package.json
|
||||
COPY packages/ui/package.json packages/ui/package.json
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
ARG GEOLIBRE_APP_BASE=/
|
||||
ARG VITE_GEE_OAUTH_CLIENT_ID=
|
||||
ARG VITE_MAPILLARY_ACCESS_TOKEN=
|
||||
ENV GEOLIBRE_APP_BASE=${GEOLIBRE_APP_BASE}
|
||||
ENV VITE_GEE_OAUTH_CLIENT_ID=${VITE_GEE_OAUTH_CLIENT_ID}
|
||||
ENV VITE_MAPILLARY_ACCESS_TOKEN=${VITE_MAPILLARY_ACCESS_TOKEN}
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# Runtime image bundles the static web app (served by nginx) and the optional
|
||||
# Python conversion/Whitebox sidecar (uvicorn), reverse-proxied at /sidecar.
|
||||
# A glibc base (not alpine/musl) is required for the prebuilt geo wheels
|
||||
# (duckdb, rasterio/rio-cogeo, freestiler, whitebox-workflows).
|
||||
FROM python:3.12-slim-bookworm AS runtime
|
||||
|
||||
# TARGETARCH is provided by BuildKit (amd64 / arm64).
|
||||
ARG TARGETARCH
|
||||
|
||||
# libexpat1 is a runtime dependency of rasterio (pulled in by rio-cogeo) that
|
||||
# the slim base image does not ship. openssl provides `openssl passwd` used by
|
||||
# entrypoint.sh to hash the optional Basic Auth password.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nginx libexpat1 openssl \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# Install the sidecar package plus the core conversion stack. duckdb and
|
||||
# rio-cogeo (rasterio) publish linux/arm64 wheels, so Vector->GeoParquet,
|
||||
# CSV->GeoParquet and Raster->COG work on both architectures.
|
||||
COPY backend/geolibre_server /opt/geolibre_server
|
||||
RUN pip install --no-cache-dir /opt/geolibre_server \
|
||||
&& pip install --no-cache-dir "duckdb>=1.1.0" "rio-cogeo>=5.0.0"
|
||||
|
||||
# freestiler (PMTiles) and whitebox-workflows publish no linux/arm64 wheels, so
|
||||
# they are installed on amd64 only. On arm64 those tools report unavailable
|
||||
# while the other conversions keep working.
|
||||
RUN if [ "$TARGETARCH" = "amd64" ]; then \
|
||||
pip install --no-cache-dir "freestiler>=0.1.0" "whitebox-workflows>=2.0.2"; \
|
||||
else \
|
||||
echo "Skipping freestiler + whitebox-workflows on $TARGETARCH (no wheels)"; \
|
||||
fi
|
||||
|
||||
# Point the sidecar at this interpreter so it skips the managed-runtime
|
||||
# bootstrap and uses the prebaked packages. Confine conversion reads/writes to
|
||||
# /data by default: the sidecar is reachable same-origin through the nginx
|
||||
# proxy, so without this an arbitrary same-origin caller could read or
|
||||
# overwrite container paths. Mount input files at /data (read-write for
|
||||
# outputs); override GEOLIBRE_CONVERSION_ROOTS to widen or disable.
|
||||
ENV GEOLIBRE_CONVERSION_PYTHON=/usr/local/bin/python \
|
||||
WBW_EXTERNAL_PYTHON=/usr/local/bin/python \
|
||||
GEOLIBRE_CONVERSION_ROOTS=/data
|
||||
RUN mkdir -p /data
|
||||
|
||||
# WARNING: docker/nginx.conf's CSP allows http://localhost:* / http://127.0.0.1:*
|
||||
# (and ws:// equivalents) in connect-src for local-dev data sources (PMTiles/COGs
|
||||
# from a dev server on another port). This image is intended for local/single-user
|
||||
# use; on a public host those allowances let the served JS probe each visitor's
|
||||
# loopback. Drop them from the CSP before publishing publicly.
|
||||
# Ship nginx.conf as an immutable template (not loaded directly). entrypoint.sh
|
||||
# renders it to /etc/nginx/conf.d/default.conf on every boot, substituting the
|
||||
# per-launch sidecar token, so a container restart never keeps a stale token.
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf.template
|
||||
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh \
|
||||
# Default auth snippet (disabled). entrypoint.sh rewrites it at start based
|
||||
# on GEOLIBRE_AUTH_USER/GEOLIBRE_AUTH_PASSWORD; baking a valid default keeps
|
||||
# `nginx -t` and non-entrypoint invocations working.
|
||||
&& printf '# Basic Auth disabled (GEOLIBRE_AUTH_USER/GEOLIBRE_AUTH_PASSWORD not set).\n' > /etc/nginx/geolibre-auth.conf
|
||||
COPY --from=build /app/apps/geolibre-desktop/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
# /healthz is exempt from the optional Basic Auth, so the check keeps passing
|
||||
# when GEOLIBRE_AUTH_USER/GEOLIBRE_AUTH_PASSWORD are set.
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
|
||||
CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1/healthz', timeout=4).status==200 else 1)" || exit 1
|
||||
|
||||
CMD ["/usr/local/bin/entrypoint.sh"]
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Qiusheng Wu
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,647 @@
|
||||
# GeoLibre
|
||||
|
||||
[](https://web.geolibre.app/?url=https://share.geolibre.app/giswqs/3d-tiles.geolibre.json)
|
||||
[](https://share.geolibre.app)
|
||||
[](https://plugins.geolibre.app)
|
||||
[](https://pypi.python.org/pypi/geolibre)
|
||||
[](https://colab.research.google.com/github/opengeos/GeoLibre/blob/main/python/examples/getting-started.ipynb)
|
||||
[](https://anaconda.org/conda-forge/geolibre)
|
||||
[](https://github.com/conda-forge/geolibre-feedstock)
|
||||
[](https://codesandbox.io/p/github/opengeos/geolibre)
|
||||
[](https://stackblitz.com/github/opengeos/geolibre)
|
||||
[](https://apps.microsoft.com/detail/9nwt67rv531x)
|
||||
[](https://aur.archlinux.org/packages/geolibre-bin)
|
||||
[](https://flatpark.org/apps/app.geolibre.GeoLibre/)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://doi.org/10.5281/zenodo.20785400)
|
||||
|
||||
|
||||
A free and open-source, lightweight, cloud-native GIS platform for visualizing, exploring, and analyzing geospatial data. It runs everywhere you do, in the web browser, on the desktop, on mobile, and inside Jupyter notebooks, all while keeping your data local and private.
|
||||
|
||||
GeoLibre is built with **Tauri v2**, **React**, **TypeScript**, **MapLibre GL JS**, **DuckDB-WASM Spatial**, and **deck.gl**. The same workspace runs as a native desktop app, a native Android app, in any modern web browser, and adapts responsively to mobile and small screens.
|
||||
|
||||
[](https://web.geolibre.app/?url=https://share.geolibre.app/giswqs/3d-tiles.geolibre.json)
|
||||
|
||||
**Video tutorials:**
|
||||
|
||||
- [GeoLibre 1.0: A Free, Open-Source Cloud-Native GIS That Runs Anywhere (Browser, Desktop & Jupyter)](https://youtu.be/87Cm0QagtxI)
|
||||
- [Geoprocessing in the Browser: 700+ Free GIS Tools in GeoLibre, Zero Install](https://youtu.be/W32bIQO_nG8)
|
||||
|
||||
## Features (v2.0)
|
||||
|
||||
- Runs across desktop (Tauri), web (browser), native Android (Tauri v2 mobile), and mobile or small screens, with a responsive, touch-friendly layout that adapts menus, dialogs, and panels (on phones the Layers/Style panels overlay the map as slide-over sheets), plus per-panel visibility through Layout settings
|
||||
- MapLibre map workspace with OpenFreeMap, Protomaps, EOX Sentinel-2 cloudless, and Openbasiskaart basemaps, planetary basemaps for Mars and the Moon (OpenPlanetaryMap) plus Mercury, Venus, the Galilean moons (Io, Europa, Ganymede, Callisto), Titan, Pluto, and Charon (USGS Astrogeology, reprojected to Web Mercator by the tiles Worker) with a per-project ellipsoid whose radius drives distance, area, and scale measurements and a planet switcher in the Layers panel, stacking of multiple raster basemaps, blank background support, double-click to swap the core basemap from the layer panel, a right-click context menu for reading coordinates and quick actions, a Gridlines coordinate-grid overlay with edge labels and a UTM easting/northing grid mode, and toggleable navigation, fullscreen, geolocation, globe, terrain, scale, attribution, and logo controls, plus a View menu with viewport history navigation, a reset pitch and bearing control, a distinct north arrow, and View in Google Maps and View in Google Earth actions, and a double-click terrain control for setting vertical exaggeration
|
||||
- Multi-map grid that splits the workspace into a grid of synchronized map views, so you can compare basemaps, layers, or time steps side by side, with any pane switchable to an optional CesiumJS 3D globe (camera-synced with the 2D maps; requires a Cesium Ion token — see [Environment variables](#environment-variables))
|
||||
- Load local vector layers supported by DuckDB-WASM Spatial, including common formats such as GeoJSON, GeoParquet, GeoPackage, Shapefile, FlatGeobuf, KML/KMZ (honoring embedded symbology, rendering GroundOverlay images as map overlays that animate through the Time Slider when time-tagged, and displaying embedded Collada `.dae` 3D models), GML, delimited text (including CSV without coordinates loaded as a standalone attribute table), GPX, and OpenStreetMap PBF extracts (parsed in-browser with osmix)
|
||||
- Reproject vector layers to EPSG:4326 on load, render vector layers that carry Z coordinates in true 3D rather than flattening them onto the ground plane, and split dragged GPX files into named waypoint, track, and route layers
|
||||
- Large local vector layers render through client-side vector tiling, with a warning before loading very large files
|
||||
- Add Data menu for XYZ tiles, WMS and WFS (with layers and feature types discovered from the service's GetCapabilities so you pick from a populated dropdown), GeoJSON URLs, GeoRSS feeds (URL or file), vector tiles (including OGC API - Tiles vector tile services), COG and GeoTIFF rasters, Cloud-Optimized NetCDF/HDF (via kerchunk references) plus local HDF5 and NetCDF-4 files, MBTiles, ArcGIS FeatureServer and VectorTileServer layers, PMTiles, Zarr, LiDAR, 3D Tiles (including authenticated tilesets via custom request headers), ArcGIS I3S scene layers (Integrated Mesh and 3D Object layers rendered on deck.gl), CAD drawings (DXF/DWG) with a drawing-layer picker and CRS selector, Gaussian splats, glTF/GLB 3D models placed at coordinates, georeferenced video overlays, and geotagged photos imported as a point layer from their EXIF GPS (with manual placement and drag for photos lacking coordinates), with a fully internationalized dialog, comma decimal support, drag-and-dropped CSV coordinate files, sample-data dropdowns on every upstream-backed panel for loading ready-made example datasets, and a saved service library for storing and re-adding frequently used web-service endpoints
|
||||
- Deck.gl Layer builder for composing deck.gl overlays from uploaded files or remote URLs
|
||||
- Cloud data integrations through the Planetary Computer and Earth Engine panels, the Overture Maps plugin, and federal Web Services plugins
|
||||
- Manual and automatic refresh for WFS, GeoJSON URL, and Add Vector Layer URL layers
|
||||
- Layer panel for visibility, opacity, reordering, rename, zoom-to-layer, identify, labels, open attribute table, export, and remove actions, with collapsible layer groups/folders for organizing the layer stack and a Search places box in the footer for geocoding to a location without leaving the panel
|
||||
- Live style panel with single, categorized, graduated, expression, and rule-based (filter-driven) symbology (fill, stroke, opacity, circle radius), proportional symbols, fill patterns, a built-in marker library, plus point heatmap and clustering renderers — all including for Add Vector Layer point layers, plus an inline color ramp picker that previews each colormap's gradient on the trigger and beside every option, and a transparent (no fill / no outline) option in the color picker, with vector layer symbology that imports and exports as OGC SLD, QGIS QML, and Mapbox GL style JSON so styles round-trip between GeoLibre, QGIS, and the Mapbox/MapLibre ecosystem
|
||||
- Label engine for labeling vector features by any attribute, with ArcGIS-style placement and styling controls (anchor, X/Y offset, rotation, wrap width, letter case), a Duplicate labels option, and unique/concatenate modes that collapse points stacked at the same coordinate into a single deduplicated label
|
||||
- Attribute table with filtering, sorting, resize controls, feature highlighting with Ctrl- and Shift-click multi-row selection, optional zoom to selected features, add-field and field-calculator tools (including geometry length and area calculation), a Charts panel (histogram, scatter, bar, line, box), a field statistics summary panel, column management (rename, delete, hide/show, reorder) with a column explorer for finding and toggling fields in wide tables, virtualized rows for large layers, and export to GeoJSON/GeoParquet/Shapefile/GeoPackage/CSV
|
||||
- SQL Workspace for running DuckDB Spatial SQL against loaded layers, local files, and remote URLs, docked as a resizable panel beside the map with editor autocomplete for tables, columns, and SQL keywords, sample queries, query history, and adding results to the map or exporting them, plus an in-browser PostGIS SQL engine via PGlite and an Apache Sedona spatial SQL engine
|
||||
- Multiple DuckDB SQL query-result layers with identify, selection, and attribute table support
|
||||
- Controls menu with Measure, Bookmark, Minimap, View State, and Map annotation tools (draw text, arrows, and highlights on the map, saved with the project), a Search panel, persistent mode banners for the Directions and Reverse Geocode tools, a Camera Tour recorder that captures an animated keyframe tour to video (with per-keyframe recapture, per-keyframe hold and transition duration controls, and saving/loading a named tour setup as JSON), a Dashboard panel of configurable chart widgets that summarize the loaded layers, and a Print menu with a print layout composer (user-editable legend, explicit map-scale input, title block with editable title and footer, page-size controls, a custom print extent, and Copy to Clipboard) that exports the map to PNG or PDF
|
||||
- Weather menu with live cloud and precipitation radar overlays (RainViewer), a Clouds overlay in the Controls menu, and a Google Earth-style sun position simulation that lights the scene for a given date and time
|
||||
- Bookmarks that capture the active layers alongside the camera, organized into folders, with selectable export, a resizable and reorderable panel, and a save-as name prompt
|
||||
- Field Collection tool for capturing point, line, and polygon observations with a per-layer custom form (text/number/date/choice fields and an optional photo), placed by device GPS or by tapping the map, written to a GeoJSON layer that flows into the attribute table, export, and offline use
|
||||
- Story map builder that composes its chapters directly on the live map, with a presenter view, dedicated start and closing slides, an optional hide-itinerary toggle, a printable PDF handout generator (with subtitle and byline fields), and standalone HTML export
|
||||
- Real-time multi-user collaboration (MVP; requires the `VITE_GEOLIBRE_COLLAB_URL` build variable — see [docs/collaboration.md](docs/collaboration.md)) so several people can edit the same project together, with per-participant permissions, an in-app chat panel, and an on-canvas session-status badge and roster (live dot, connected-participant count, and an expandable client list) while a session is active
|
||||
- Natural-language GIS assistant that turns plain-English requests into auditable, undoable GeoLibre operations (Spatial SQL, symbology, add/remove data, and map control), provider-pluggable with your own API key (also read from OS environment variables) and a dedicated AI Providers settings section with per-feature provider dropdowns
|
||||
- In-app Python Console plus a Python automation API for scripting the app
|
||||
- Notebook panel docked beside the map for running Jupyter against the live map: the web build embeds a self-hosted JupyterLite site (in-browser Pyodide kernel) and the desktop build launches a uv-managed JupyterLab server, with notebook cells driving the map through an auto-loaded `geolibre` client. See [Notebook Panel](docs/notebook.md)
|
||||
- Command palette (`Ctrl`/`Cmd` + `K`) that searches and runs menu and toolbar actions across Add Data, Processing, Controls, Plugins, and Help, global keyboard shortcuts for New/Open/Save/Save As and Google Earth-style camera resets (`N` north up, `U` top-down, `R` reset view), and a `?` shortcuts cheat sheet
|
||||
- Conversion menu for Vector to GeoParquet/FlatGeobuf/PMTiles, a generic Vector to Vector converter that translates between any supported vector formats by file extension, CSV to GeoParquet, and Raster to COG; GeoParquet and CSV conversions run in the browser with DuckDB-WASM, while FlatGeobuf, PMTiles, and COG require the optional Python sidecar
|
||||
- Whitebox toolbox that runs entirely in the browser through a WebAssembly runtime with raster I/O (no Python sidecar required), surfacing both Whitebox tools and GeoLibre's own WASM raster tools, browsable by category directly in the Processing menu with nested subcategory submenus and an offline-bundled tool catalog, with batch tools run against a selected input directory
|
||||
- Vector menu with common geometry and analysis tools (buffer, centroids, convex hull, dissolve, bounding box, simplify, clip, intersection, difference, union, spatial join, attribute join, select by value, select by location, movement, space-time, and cell coverage) that run in the browser with Turf.js, an optional GeoPandas sidecar engine for every tool, and an in-browser GeoPandas engine via Pyodide (no server, same results as the sidecar)
|
||||
- Raster menu with common raster tools (hillshade, slope, aspect, reproject, resample, clip by extent, clip by mask layer, polygonize, contour, zonal statistics, raster calculator, reclassify, mosaic, focal statistics) backed by a rasterio Python sidecar, with a client-side fallback so core tools also run in the browser when no sidecar is available, plus in-browser extraction of COG, WMS, and XYZ bounding-box subsets and a normalized-difference index builder for any HTTP COG
|
||||
- Spectral Index toolbox (NDVI, GNDVI, NDWI, NDMI, NDBI, NBR, EVI, SAVI) with Sentinel-2, Landsat 8-9, NAIP, and custom band layouts, evaluated client-side with geotiff.js or on the rasterio sidecar
|
||||
- Spatial Statistics toolbox and a Processing batch runner with model/pipeline chaining to run a sequence of tools as one job
|
||||
- Raster Georeferencer (Processing → Georeferencing) that pins a non-georeferenced image to the map with ground control points using a least-squares affine fit, reporting per-GCP and RMS residuals
|
||||
- Single-band pseudocolor with classification, reversed and custom color ramps, the full colormap list shown as inline gradient swatches in the Color ramp picker, a Legend populated automatically from a paletted raster's embedded color table, and RGB band combination for styling raster layers, plus COG pixel-value inspection from the Identify icon
|
||||
- Network analysis tools for isochrones, service areas, origin–destination (OD) cost matrices, and sequential routes (directions) through an ordered set of waypoints
|
||||
- Geocoding tools for forward, batch, and reverse geocoding through a multi-provider abstraction
|
||||
- AI Segmentation (SamGeo) that turns imagery into vector features with [segment-geospatial](https://github.com/opengeos/segment-geospatial) and Meta's SAM 3 — text prompts ("trees", "buildings") or automatic segmentation, proxied to a separate `samgeo-api` model server (GPU recommended)
|
||||
- H3 tools to create hexagonal grids over an extent and bin point layers into H3 cells
|
||||
- Undo/redo for layer and style operations
|
||||
- Drag and drop vector and GeoTIFF/COG raster files onto the map to add them as layers
|
||||
- Project menu to create, open, save, and Save As `.geolibre.json` projects, export a project to a single standalone interactive HTML file that runs offline with no server, and a project gallery for browsing and opening shared projects with one click
|
||||
- Desktop diagnostics panel (capturing native Tauri HTTP requests in the network log and classifying failed `fetch()` errors), a guided update workflow with a startup update check and update preferences, and MSIX packaging support, with macOS installers signed with an Apple Developer ID certificate and notarized by Apple so they open without a Gatekeeper workaround, plus Windows Package Manager (winget) distribution as `OpenGeos.GeoLibre` and a Windows portable zip build that runs without installation
|
||||
- Customizable UI profiles that tailor which menus, panels, and data sources are visible, so a deployment can present a focused subset of the app to its users. See [UI Profiles](docs/ui-profiles.md)
|
||||
- Plugin system with basemap, layer control, MapLibre components, swipe, street view, Mapillary coverage and street-level image viewer, Historical Imagery, Elevation Profile, Overture Maps, USGS LiDAR, GeoAgent, and GeoEditor integrations (the GeoEditor can pull the vector features currently visible in the map view into the editor for editing without re-importing the source, and write edits back to their origin, including GeoPackage and GeoJSON files and PostGIS database tables), including configurable control positions and external plugin manifests; external plugins can render on the host's shared deck.gl instance via `app.getDeckGL()`, use the maplibre-gl-raster stack and the map projection control, register native raster and tile layers, register first-class right-sidebar panels, toolbar menus, and floating panels through the plugin UI host API (including a shared-rail replace-style dock mode), and place their toolbar menus after the Help menu
|
||||
- Time Slider plugin for animating time series raster and vector data, including binding existing vector layers already on the map to the timeline and a pixel time-series chart that plots a sampled pixel's value across a raster stack
|
||||
- Atmosphere Effects plugin that renders a deep-space backdrop, parallax starfield, comets, and an atmospheric halo around the globe at low zoom (technique adapted from [Leonel Dias](https://leoneljdias.github.io/posts/globe-atmosphere-halo-comets/)), with a Spinning Globe panel and customizable atmosphere halo and deep-space colors
|
||||
- Directions plugin for interactive routing via [maplibre-gl-directions](https://github.com/maplibre/maplibre-gl-directions): click the map to add waypoints, drag to reposition, and click a waypoint to remove it (uses the public OSRM demo server, driving only)
|
||||
- Install external plugins from an uploaded zip on both desktop and web, plus external plugin zip loading from the app data plugins directory and local development plugin directories, with the Manage Plugins list sorted alphabetically
|
||||
- Bundled drop-in plugins under `public/plugins/<id>/` that bake into both the web and desktop builds and load automatically with no manifest URL
|
||||
- Browser deployment with Docker, embed-friendly URL parameters (including `?url=` project deep links that skip the welcome wizard and a `?welcome=0` param to opt out of onboarding), and a `maponly` chrome-free mode
|
||||
- Native Android app built from the same codebase with Tauri v2 mobile, producing signed, per-architecture APKs (~40 MB) through a GitHub Actions workflow; tools that depend on a local desktop process (Whitebox, Raster, Conversion, AI Segmentation, PostgreSQL/Martin) are hidden on mobile so nothing is shown that cannot run. See [Android](docs/android.md)
|
||||
- Installable, offline-capable Progressive Web App (PWA) build, plus a **Download Offline Area** tool that pre-caches the current map view's basemap tiles, and service-worker caching of the CDN-loaded Pyodide and PGlite/PostGIS engines so browser SQL and Python keep working offline after first use
|
||||
- Internationalization framework with react-i18next and 13 complete per-build translation catalogs, plus a `?locale`/`?lang` query parameter to set the embed language
|
||||
- Accessibility pass with axe-checked screens, keyboard navigation, and screen-reader labels
|
||||
- App-wide, section, and plugin React error boundaries that contain failures and keep the rest of the workspace usable
|
||||
- Python package (`geolibre`) that embeds the full app in Jupyter notebooks as an [anywidget](https://anywidget.dev), with an expanded leafmap-style API (local raster, marker/cluster, and choropleth layers; `split_map`, `add_legend`, and `add_colorbar` helpers; typed read-back of selected/drawn features; and `to_html` export) and two-way project sync
|
||||
- Optional Python FastAPI sidecar for heavier processing workflows
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js** 22+
|
||||
- **Rust** toolchain ([rustup](https://rustup.rs/)) for Tauri desktop builds
|
||||
- Linux: `webkit2gtk`, `libayatana-appindicator` (see [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/))
|
||||
|
||||
## Install
|
||||
|
||||
Prebuilt desktop installers for Linux, Windows, and macOS are published on the
|
||||
[Releases](https://github.com/opengeos/GeoLibre/releases) page. On Windows you
|
||||
can install the signed, auto-updating build from the
|
||||
[Microsoft Store](https://apps.microsoft.com/detail/9nwt67rv531x), or the
|
||||
unsigned GitHub Release build via `winget install OpenGeos.GeoLibre`. On macOS
|
||||
you can install and update with Homebrew — GeoLibre is in the official
|
||||
[Homebrew Cask](https://github.com/Homebrew/homebrew-cask/blob/main/Casks/g/geolibre.rb)
|
||||
repository, so no tap is required:
|
||||
|
||||
```bash
|
||||
brew install --cask geolibre
|
||||
```
|
||||
|
||||
The macOS app is signed with an Apple Developer ID certificate and notarized by
|
||||
Apple, so it launches normally with no quarantine workaround. See
|
||||
[Downloads](docs/downloads.md) for details and the manual install steps.
|
||||
|
||||
To build from source instead:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/opengeos/GeoLibre.git
|
||||
cd GeoLibre
|
||||
npm install
|
||||
```
|
||||
|
||||
Bun users can run `bun install`. The root `trustedDependencies` list allows the known install scripts for `core-js`, `@google/genai`, and `protobufjs`.
|
||||
|
||||
## Update
|
||||
|
||||
To update an existing source checkout to the latest version, pull the changes, reinstall dependencies (in case `package.json` changed), and rebuild:
|
||||
|
||||
```bash
|
||||
cd /path/to/GeoLibre # your GeoLibre checkout
|
||||
git pull origin main
|
||||
npm install # or: bun install
|
||||
```
|
||||
|
||||
If you run a production build, rebuild afterwards with `npm run build` (web) or `npm run tauri:build` (desktop). If you work from the dev servers (`npm run dev` or `npm run tauri:dev`), the `git pull` and `npm install` above are enough — just restart the dev server to pick up the changes.
|
||||
|
||||
## Run (web dev, map in browser)
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open http://localhost:5173. The map and browser vector import support local vector files that DuckDB-WASM Spatial can read, including common formats such as GeoJSON, GeoParquet, GeoPackage, Shapefile, FlatGeobuf, KML/KMZ, and GML, with direct handling for GeoJSON, zipped Shapefiles, and KMZ archives. You can choose files from Add Vector Layer or drag them onto the app. GeoTIFF/COG rasters can also be dragged onto the map to add them as raster layers. Desktop filesystem dialogs, local MBTiles, and local raster file reads require Tauri.
|
||||
|
||||
## Run with Docker
|
||||
|
||||
Build and run the browser version of GeoLibre:
|
||||
|
||||
```bash
|
||||
docker build -t geolibre .
|
||||
docker run --rm -p 8080:80 geolibre
|
||||
```
|
||||
|
||||
Open http://localhost:8080. The Docker image serves the production Vite build with nginx. Desktop-only features such as Tauri filesystem dialogs, local MBTiles, local raster file reads, and project save/open require the desktop app.
|
||||
|
||||
### Bundled conversion sidecar
|
||||
|
||||
The image also bundles the Python conversion/Whitebox sidecar (uvicorn) and
|
||||
reverse-proxies it at `/sidecar`, so the browser reaches it same-origin with no
|
||||
CORS or separate process to manage. `/conversion/status` is reachable at
|
||||
`http://localhost:8080/sidecar/conversion/status`.
|
||||
|
||||
- **Vector → GeoParquet** and **CSV → GeoParquet** run in the browser with
|
||||
DuckDB-WASM and need no sidecar.
|
||||
- **Vector → FlatGeobuf**, **Vector → PMTiles**, and **Raster → COG** use the
|
||||
sidecar. These read a file **path on the sidecar's filesystem**, so from a
|
||||
pure browser they currently work for files mounted into the container (a
|
||||
browser cannot hand the container an absolute path); upload-based input is a
|
||||
planned follow-up. The desktop app passes real local paths, so all
|
||||
conversions work there.
|
||||
- **PMTiles** and **Whitebox** are **amd64-only** in the container —
|
||||
`freestiler` and `whitebox-workflows` publish no linux/arm64 wheels. On arm64
|
||||
the other conversions still work; those two report unavailable.
|
||||
|
||||
Because the sidecar is reachable same-origin, conversion reads/writes are
|
||||
confined to `GEOLIBRE_CONVERSION_ROOTS` (default `/data` in the image). Mount
|
||||
your files there:
|
||||
|
||||
```bash
|
||||
docker run --rm -p 8080:80 -v "$PWD/data:/data" geolibre
|
||||
```
|
||||
|
||||
Set `GEOLIBRE_DISABLE_SIDECAR=1` to run nginx only (the original web-only
|
||||
behavior):
|
||||
|
||||
```bash
|
||||
docker run --rm -p 8080:80 -e GEOLIBRE_DISABLE_SIDECAR=1 geolibre
|
||||
```
|
||||
|
||||
### Password protection (optional)
|
||||
|
||||
Set `GEOLIBRE_AUTH_USER` and `GEOLIBRE_AUTH_PASSWORD` to put the whole
|
||||
container (the app and the `/sidecar` API) behind HTTP Basic Auth:
|
||||
|
||||
```bash
|
||||
docker run --rm -p 8080:80 \
|
||||
-e GEOLIBRE_AUTH_USER=admin \
|
||||
-e GEOLIBRE_AUTH_PASSWORD='change-me' \
|
||||
geolibre
|
||||
```
|
||||
|
||||
The browser prompts for the credentials on first visit. `/healthz` stays
|
||||
unauthenticated so the container health check keeps working. When the
|
||||
variables are unset (the default), no authentication is applied.
|
||||
|
||||
As with any Docker env var, a password passed with `-e` lands in your shell
|
||||
history and is readable on the host via `docker inspect`. Beyond quick local
|
||||
testing, prefer `--env-file` with a permission-restricted file, or a secrets
|
||||
manager.
|
||||
|
||||
This is a single shared credential, not per-user accounts. Basic Auth sends
|
||||
credentials with every request, so on anything beyond a trusted local network
|
||||
put a TLS-terminating reverse proxy (Caddy, Traefik, nginx) in front of the
|
||||
container. For multi-user or SSO needs, use an auth proxy such as
|
||||
`oauth2-proxy` or Authelia in front of the unmodified image instead. Also see
|
||||
the note in `docker/nginx.conf` about dropping the `localhost` CSP allowances
|
||||
before exposing the image publicly.
|
||||
|
||||
The published image is available from GitHub Container Registry:
|
||||
|
||||
```bash
|
||||
docker pull ghcr.io/opengeos/geolibre:latest
|
||||
docker run --rm -p 8080:80 ghcr.io/opengeos/geolibre:latest
|
||||
```
|
||||
|
||||
For deployments under a URL subpath, pass `GEOLIBRE_APP_BASE` at build time:
|
||||
|
||||
```bash
|
||||
docker build --build-arg GEOLIBRE_APP_BASE=/geolibre/ -t geolibre .
|
||||
```
|
||||
|
||||
The container always serves the app from its root path. The build argument only sets the URL prefix that the app expects, so subpath deployments also require a reverse proxy in front of the container that strips the prefix before forwarding requests (for example, nginx `proxy_pass http://geolibre/;` with a trailing slash).
|
||||
|
||||
## SQL Workspace
|
||||
|
||||
The SQL Workspace runs DuckDB SQL (with the Spatial extension loaded, so `ST_*` functions are available) directly in the browser against your loaded layers and remote data. Open it from the Processing menu.
|
||||
|
||||
- **Query loaded layers.** Every vector layer with in-memory features is exposed as a table; the queryable table names are listed at the top of the dialog.
|
||||
- **Read files and URLs.** Use `read_parquet()`, `read_csv_auto()`, `read_json_auto()`, or `ST_Read()`. A bare URL or path after `FROM`/`JOIN` (for example `SELECT * FROM https://host/data.parquet`) is auto-wrapped in the matching reader. Remote files are streamed over HTTP range requests, so large datasets are not downloaded in full.
|
||||
- **Sample queries.** A dropdown of ready-to-run examples (attribute-only, aggregate, and spatial queries) against a public sample dataset, plus a per-layer "sample query for layer" dropdown.
|
||||
- **Query history.** Recently run queries are saved (in `localStorage`) and can be reloaded from the History dropdown.
|
||||
- **Results and export.** Results show in a grid (capped for display; the full result is kept for export). When a query returns a geometry column, you can add the result to the map as a new layer (with an optional custom **layer name**) or export it as CSV or GeoParquet.
|
||||
|
||||
```sql
|
||||
SELECT NAME, CONTINENT, POP_EST, geom
|
||||
FROM https://data.source.coop/giswqs/opengeos/countries.parquet
|
||||
WHERE POP_EST > 50000000
|
||||
ORDER BY POP_EST DESC;
|
||||
```
|
||||
|
||||
Only a single statement is supported per run. Cloud object-store URLs (`s3://`, `gs://`, `az://`) are transparently rewritten to their public HTTPS equivalents, so they work for anonymous / public datasets.
|
||||
|
||||
## Vector tools
|
||||
|
||||
The **Processing → Vector** menu opens a single Vector tools dialog with common geometry operations that run against your loaded GeoJSON layers. Pick a tool, choose the input layer (and an overlay layer for the two-layer tools), set the parameters, and the result is added to the map as a new layer.
|
||||
|
||||
- **Geometry tools.** **Buffer** (by a distance in kilometers, meters, or miles), **Centroids** (one centroid point per feature), **Convex hull** (a single polygon enclosing all features), **Dissolve** (merge polygons, optionally grouped by an attribute field), **Bounding box** (the rectangular envelope of all features), **Simplify** (Douglas-Peucker vertex reduction), **Smooth** (spline-based line/polygon smoothing), **Regular grid** (a point or polygon grid over an extent), and **Voronoi / Delaunay** (Voronoi polygons or a Delaunay triangulation from a point layer).
|
||||
- **Overlay tools.** **Clip** (clip the input to an overlay layer, keeping input attributes), **Intersection**, **Difference**, and **Union** between two polygon layers.
|
||||
- **Join tools.** **Spatial join** (attach a join layer's attributes to each input feature by a spatial relationship — intersects, within, or contains — with an inner or left join, for any geometry type) and **Attribute join** (attach a join table's attributes by a matching key field, no geometry — e.g. join census stats to boundary polygons — choosing which fields to bring over, with an inner or left join).
|
||||
- **Select tools.** **Select by value** (extract features whose attribute matches a condition — =, ≠, >, ≥, <, ≤, contains, starts with, is empty/not empty) and **Select by location** (extract features by their spatial relationship to a second layer — intersects, within, contains, or disjoint) into new layers.
|
||||
- **Three engines.** Every tool runs fully in the browser with [Turf.js](https://turfjs.org/), so no sidecar is required. Every tool can also run on the optional GeoPandas sidecar for projection-aware results; when the sidecar is unavailable the dialog falls back to the client engine. A third **Python (Pyodide)** engine runs the same GeoPandas/Shapely code as the sidecar but **entirely in the browser** via [Pyodide](https://pyodide.org) — no server, so it works on the web build too. The first run lazily downloads the Python runtime from a CDN (override with `VITE_PYODIDE_INDEX_URL` to self-host for offline use); results match the sidecar because both share one `vector_ops.py`.
|
||||
|
||||
To enable the sidecar engine, install the optional `vector` extra (it is not bundled by default to keep the sidecar small):
|
||||
|
||||
```bash
|
||||
# install the vector extras (GeoPandas, Shapely)
|
||||
pip install -e "backend/geolibre_server[vector]"
|
||||
# run it
|
||||
geolibre-server # or: uvicorn geolibre_server.app.main:app --host 127.0.0.1 --port 8765
|
||||
```
|
||||
|
||||
## Raster tools
|
||||
|
||||
The **Processing → Raster** menu opens a single Raster tools dialog with common raster operations. Because raster processing cannot run in the browser, these tools run on the Python sidecar (rasterio) with a file path in and a file path out: pick a tool, choose an input raster and an output file, set the parameters, and run the job.
|
||||
|
||||
- **Terrain.** **Hillshade**, **Slope** (degrees or percent), and **Aspect** from an elevation model.
|
||||
- **Reproject.** **Reproject** to a target CRS and **Resample** to a new pixel size, with selectable resampling (nearest, bilinear, cubic).
|
||||
- **Clip.** **Clip by extent** (a bounding box in the raster's CRS) and **Clip by mask layer** (a GeoJSON mask, reprojected to the raster automatically).
|
||||
- **Raster to vector.** **Polygonize** (vector polygons grouped by pixel value) and **Contour** (contour lines from an elevation model), written as GeoJSON.
|
||||
- **Vector to raster.** **Interpolation (IDW / Kriging)** turns a point layer's numeric attribute into a continuous raster surface via inverse distance weighting or ordinary kriging.
|
||||
|
||||
The tools share the conversion sidecar job runner. Install the optional `raster` extra (rasterio is also pulled in by the `conversion` extra):
|
||||
|
||||
```bash
|
||||
# install the raster extras (rasterio, numpy, contourpy)
|
||||
pip install -e "backend/geolibre_server[raster]"
|
||||
# run it
|
||||
geolibre-server # or: uvicorn geolibre_server.app.main:app --host 127.0.0.1 --port 8765
|
||||
```
|
||||
|
||||
## AI segmentation
|
||||
|
||||
The **Processing → AI Segmentation** dialog turns imagery into vector features with [segment-geospatial](https://github.com/opengeos/segment-geospatial) (SamGeo) and Meta's **SAM 3** model: choose a GeoTIFF, type a text prompt (e.g. "trees", "buildings", "water") or run automatic segmentation, and the resulting polygons are added as a new layer.
|
||||
|
||||
The model stack does not run inside the sidecar. The sidecar exposes a thin `/ml` reverse-proxy in front of a separate `samgeo-api` server (the REST server shipped with `segment-geospatial`), which runs SAM 3 and returns GeoJSON. A CUDA GPU is strongly recommended.
|
||||
|
||||
```bash
|
||||
# install the model server (in an env with a working PyTorch build)
|
||||
pip install "segment-geospatial[api,samgeo3]"
|
||||
# install the sidecar's ml extra (just an HTTP client; models live in samgeo-api)
|
||||
pip install -e "backend/geolibre_server[ml]"
|
||||
```
|
||||
|
||||
`samgeo-api` is launched on demand when it's on the `PATH`, or point the sidecar at an existing server with `GEOLIBRE_ML_SAMGEO_URL=http://127.0.0.1:8000`. See [docs/user-guide/segmentation.md](docs/user-guide/segmentation.md) for details. Uses SAM 3.
|
||||
|
||||
## Embed the demo
|
||||
|
||||
The browser demo supports URL parameters for iframe-friendly layouts.
|
||||
|
||||
Open a project by URL:
|
||||
|
||||
<https://web.geolibre.app/?url=https://share.geolibre.app/giswqs/3d-tiles.geolibre.json>
|
||||
|
||||
Supported query parameters:
|
||||
|
||||
| Parameter | Example | Description |
|
||||
| ------------ | -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `url` | `url=https://share.geolibre.app/giswqs/3d-tiles.geolibre.json` | Loads a `.geolibre.json` project from a public URL. |
|
||||
| `layout` | `layout=compact` | Uses the compact embed layout with icon-only toolbar buttons and hidden project metadata. `embed` and `iframe` are aliases. |
|
||||
| `toolbar` | `toolbar=icons` | Shows icon-only toolbar buttons without enabling the full compact layout. |
|
||||
| `panels` | `panels=none` | Hides the Layers, Style, and Attribute table panels. `hidden`, `hide`, and `off` are aliases. |
|
||||
| `hidePanels` | `hidePanels=true` | Alternative way to hide the Layers, Style, and Attribute table panels. |
|
||||
| `maponly` | `maponly` | Hides all chrome (toolbar menu, Layers/Style/Attribute panels, and status bar), leaving only the map. The bare flag or any of `true`, `1`, `yes`, `on` enable it. |
|
||||
| `theme` | `theme=dark` | Sets the initial color theme on load, overriding the OS preference. Accepts `dark` or `light`; the in-app toggle still works afterwards. |
|
||||
|
||||
Use compact mode for narrow embeds. This shows icon-only toolbar buttons and hides project metadata:
|
||||
|
||||
```text
|
||||
https://web.geolibre.app/?url=https://share.geolibre.app/giswqs/3d-tiles.geolibre.json&layout=compact
|
||||
```
|
||||
|
||||
Hide the Layers, Style, and Attribute table panels for map-focused embeds:
|
||||
|
||||
```text
|
||||
https://web.geolibre.app/?url=https://share.geolibre.app/giswqs/3d-tiles.geolibre.json&layout=compact&panels=none
|
||||
```
|
||||
|
||||
Use `toolbar=icons` when you only want icon-only toolbar buttons. `panels=hidden`, `panels=hide`, `panels=off`, and `hidePanels=true` are accepted aliases for hiding panels.
|
||||
|
||||
For a fully chrome-free, map-only embed, use `maponly`. It hides the toolbar menu, all panels, and the status bar:
|
||||
|
||||
```text
|
||||
https://web.geolibre.app/?url=https://share.geolibre.app/giswqs/3d-tiles.geolibre.json&maponly
|
||||
```
|
||||
|
||||
## Python package (Jupyter)
|
||||
|
||||
GeoLibre ships a Python package that embeds the **full** GeoLibre app (menus,
|
||||
panels, processing tools) in a Jupyter notebook cell as an
|
||||
[anywidget](https://anywidget.dev), with a leafmap-style API. State syncs both
|
||||
ways through a single `.geolibre.json` project, so data you add from Python
|
||||
appears in the UI, and edits you make in the UI are readable back from Python.
|
||||
|
||||
```bash
|
||||
pip install geolibre
|
||||
```
|
||||
|
||||
Or with conda:
|
||||
|
||||
```bash
|
||||
conda install -c conda-forge geolibre
|
||||
```
|
||||
|
||||
```python
|
||||
from geolibre import Map
|
||||
|
||||
m = Map(center=(-100, 40), zoom=4)
|
||||
m.add_geojson("https://example.com/data.geojson", name="Data")
|
||||
m.add_tile_layer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", name="OpenStreetMap")
|
||||
m.add_cog("https://example.com/dem.tif", name="DEM")
|
||||
m # the full GeoLibre UI renders in the cell
|
||||
```
|
||||
|
||||
Read state edited in the UI, and round-trip projects:
|
||||
|
||||
```python
|
||||
m.to_project()["mapView"]["center"] # reflects the live UI view after panning
|
||||
m.save_project("my-map.geolibre.json")
|
||||
Map().load_project("my-map.geolibre.json")
|
||||
```
|
||||
|
||||
The package source lives in [`python/`](python/), and the bundled web app is
|
||||
built into the wheel by `npm run build:embed`. The interactive widget works in
|
||||
local Jupyter, VS Code, Google Colab (its built-in port proxy is used
|
||||
automatically), and JupyterHub / remote servers (through a Jupyter Server
|
||||
extension bundled with the wheel and auto-enabled on install, so managed hubs
|
||||
work without `jupyter-server-proxy`; pass `Map(server_proxy=True)` on non-Hub
|
||||
remote setups). See the [Python package guide](docs/python.md) for the full API.
|
||||
|
||||
## Environment variables
|
||||
|
||||
The Street View plugin can use Google Street View and Mapillary imagery. Create `apps/geolibre-desktop/.env.local` and set one or both provider credentials:
|
||||
|
||||
```env
|
||||
VITE_GOOGLE_MAPS_API_KEY=your_google_maps_api_key
|
||||
VITE_MAPILLARY_ACCESS_TOKEN=your_mapillary_access_token
|
||||
```
|
||||
|
||||
For Google Street View, enable the Maps Embed API for the key in Google Cloud. For Mapillary, create an app in the Mapillary developer dashboard and use its client access token.
|
||||
|
||||
The optional **Cesium 3D-globe view** — a split-pane globe rendered with [CesiumJS](https://cesium.com/platform/cesiumjs/) alongside the 2D MapLibre map — needs a [Cesium Ion](https://ion.cesium.com/) access token for its world imagery and terrain. Create a free Ion account, copy your default access token, and set it at build time:
|
||||
|
||||
```env
|
||||
CESIUM_TOKEN=your_cesium_ion_access_token
|
||||
```
|
||||
|
||||
`CESIUM_TOKEN` (or the `VITE_`-prefixed `VITE_CESIUM_TOKEN`) is read by `vite.config.ts` and baked into the build. A user can **also set it at runtime** — with no rebuild — in the Settings dialog's **Environment Variables** section, which has a dedicated masked **Cesium Ion token** field. That token is stored locally on the device (in browser storage on the web build), **not** in the shared project file, and overrides the build-time value; it is how a web user brings their own Ion token. (A free-form `VITE_CESIUM_TOKEN` variable in the same section still works and takes precedence, as an override.) Without a token from any source, the 3D-globe toggle is hidden entirely (the 2D map is unaffected). Ion access tokens are designed to ship in client bundles. See [docs/architecture.md](docs/architecture.md#3d-globe-view-cesiumjs) for how the globe integrates.
|
||||
|
||||
The optional **Python (Pyodide)** vector engine loads its runtime from the public jsDelivr CDN by default. To self-host it for offline or production use, point it at a mirrored copy of the Pyodide distribution:
|
||||
|
||||
```env
|
||||
VITE_PYODIDE_INDEX_URL=https://your-host/pyodide/v0.27.7/full/
|
||||
```
|
||||
|
||||
Similarly, the DuckDB Spatial extension is installed from DuckDB's remote
|
||||
extension repository by default. To load it from a mirror instead (so
|
||||
`INSTALL spatial` is skipped and the extension is loaded directly), set the full
|
||||
path or URL to the extension file:
|
||||
|
||||
```env
|
||||
VITE_DUCKDB_SPATIAL_EXTENSION_PATH=https://your-host/duckdb/spatial.duckdb_extension.wasm
|
||||
```
|
||||
|
||||
Both `VITE_PYODIDE_INDEX_URL` and `VITE_DUCKDB_SPATIAL_EXTENSION_PATH` can also be set at runtime through the Settings dialog's runtime environment variables (no rebuild required), so air-gapped or corporate deployments can point Pyodide and the DuckDB Spatial extension at internal mirrors without rebuilding.
|
||||
|
||||
Restart `npm run dev` or `npm run tauri:dev` after changing these values. Vite only exposes variables with the `VITE_` prefix to the frontend.
|
||||
|
||||
## Run (desktop)
|
||||
|
||||
```bash
|
||||
npm run tauri:dev
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
npm run tauri:build
|
||||
```
|
||||
|
||||
The default desktop build keeps the Linux binary small and uses DuckDB-WASM for
|
||||
DuckDB-backed browser features. To build a larger desktop binary with the native
|
||||
`duckdb-rs` vector loader enabled, run:
|
||||
|
||||
```bash
|
||||
npm run tauri:build:native-duckdb
|
||||
```
|
||||
|
||||
Where to find the output:
|
||||
|
||||
- **Web build** — static files in `apps/geolibre-desktop/dist/`. Serve this directory with any static web server (or the Docker image above).
|
||||
- **Desktop installers** — `apps/geolibre-desktop/src-tauri/target/release/bundle/`, with per-platform subfolders: `deb/`, `rpm/`, and `appimage/` on Linux; `msi/` and `nsis/` on Windows; `dmg/` and `macos/` on macOS. The unbundled executable is in `apps/geolibre-desktop/src-tauri/target/release/`. On Linux, `npm run tauri:build` builds `deb` and `rpm` by default; passing `--bundles` replaces that default selection rather than adding to it, so list every format you want, for example `npm run tauri:build -- --bundles deb,rpm,appimage` for all three.
|
||||
|
||||
## Android
|
||||
|
||||
GeoLibre builds as a native Android app from the same codebase via Tauri v2
|
||||
mobile. You need the Android SDK + NDK, a JDK (17 or 21), and the Rust Android
|
||||
targets; see [docs/android.md](docs/android.md) for the full toolchain setup,
|
||||
signing, and sideloading guide. Once set up:
|
||||
|
||||
```bash
|
||||
cd apps/geolibre-desktop
|
||||
npx tauri android init # generate the Gradle project (once)
|
||||
npm run tauri android dev # run on a connected device/emulator
|
||||
npx tauri android build --apk --split-per-abi # release APKs, ~40 MB per ABI
|
||||
```
|
||||
|
||||
Install the `arm64-v8a` APK on real phones. The CI workflow
|
||||
(`.github/workflows/android.yml`) builds and signs per-ABI release APKs on each
|
||||
published GitHub release (and on demand via the "Run workflow" button) and
|
||||
uploads them as artifacts; set the `ANDROID_KEYSTORE_*` repository secrets
|
||||
to sign with a real release key (otherwise a debug key is used for testable
|
||||
builds). Heavy tools that need the Python sidecar or local helper processes are
|
||||
hidden on mobile.
|
||||
|
||||
## Quality checks
|
||||
|
||||
Run the fast TypeScript unit tests:
|
||||
|
||||
```bash
|
||||
npm run test:frontend
|
||||
```
|
||||
|
||||
Run the full local quality gate:
|
||||
|
||||
```bash
|
||||
npm run ci
|
||||
```
|
||||
|
||||
## Optional Python sidecar
|
||||
|
||||
```bash
|
||||
cd backend/geolibre_server
|
||||
python -m venv .venv && source .venv/bin/activate
|
||||
pip install -e .
|
||||
uvicorn geolibre_server.app.main:app --host 127.0.0.1 --port 8765
|
||||
```
|
||||
|
||||
### Conversion tools (Processing → Conversion)
|
||||
|
||||
The **Processing → Conversion** menu (Vector → GeoParquet / FlatGeobuf,
|
||||
CSV → GeoParquet, Vector → PMTiles, Raster → COG) talks to this sidecar at
|
||||
`http://127.0.0.1:8765`. **Vector → GeoParquet** and **CSV → GeoParquet** also
|
||||
run fully in the browser with DuckDB-WASM and need no sidecar; the others
|
||||
require it.
|
||||
|
||||
To use them from the **web** build, start the sidecar and serve the app from
|
||||
`localhost:5173` (CORS is restricted to that origin and the Tauri origins):
|
||||
|
||||
```bash
|
||||
# install the conversion extras (DuckDB, rio-cogeo, freestiler)
|
||||
pip install -e "backend/geolibre_server[conversion]"
|
||||
# run it
|
||||
geolibre-server # or: uvicorn geolibre_server.app.main:app --host 127.0.0.1 --port 8765
|
||||
```
|
||||
|
||||
The sidecar self-bootstraps a managed runtime on first use; set
|
||||
`GEOLIBRE_CONVERSION_PYTHON=$(which python)` to reuse the current environment
|
||||
instead. See [backend/geolibre_server/README.md](backend/geolibre_server/README.md)
|
||||
for details.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
apps/geolibre-desktop # Tauri + React app
|
||||
packages/core # Types, store, project format
|
||||
packages/map # MapLibre integration
|
||||
packages/ui # Tailwind + shadcn/ui
|
||||
packages/plugins # Plugin API
|
||||
packages/processing # Algorithm registry
|
||||
backend/geolibre_server # FastAPI sidecar
|
||||
python/ # geolibre Python package (Jupyter anywidget)
|
||||
docs/ # Architecture & API docs
|
||||
```
|
||||
|
||||
## Add a plugin
|
||||
|
||||
Built-in plugins live in `packages/plugins/src/plugins/` and are registered by the desktop app in `apps/geolibre-desktop/src/hooks/usePlugins.ts`. Map control plugins can expose a control position through `getMapControlPosition()` and `setMapControlPosition()` so the Plugins menu can move them between map corners.
|
||||
|
||||
For external plugin development, start from the [GeoLibre plugin template](https://github.com/opengeos/geolibre-plugin-template). It includes a `plugin.json` manifest, a GeoLibre plugin wrapper entry point, and a `package:geolibre` script that creates a zip file for the desktop app data `plugins/` directory. During development, Settings → Manage Plugins can scan an additional local plugin directory, including an unpacked bundle folder such as the template's `geolibre-plugin/` directory, or a hosted `plugin.json` manifest URL. See the [Plugin API](docs/plugin-api.md) for the external plugin contract.
|
||||
|
||||
To bake an external plugin into the build so it loads automatically — with no Settings entry and no manifest URL — drop its built folder into `apps/geolibre-desktop/public/plugins/<plugin-id>/` (the same `plugin.json` + `dist/` a manifest URL would serve). The `bundledPlugins()` Vite plugin discovers it at build time and the app loads it through the normal external-plugin path. The same folder serves both the web build and the desktop build (which ships the same frontend), so one drop-in covers both. Private plugin bundles are git-ignored under that folder and copied in at build/deploy time. See the [Plugin API](docs/plugin-api.md#bundled-plugins-baked-into-the-build) for details and the security model.
|
||||
|
||||
1. Create a plugin file in `packages/plugins/src/plugins/`.
|
||||
|
||||
```typescript
|
||||
import type { GeoLibreAppAPI, GeoLibrePlugin } from "../types";
|
||||
|
||||
export const myPlugin: GeoLibrePlugin = {
|
||||
id: "my-plugin",
|
||||
name: "My Plugin",
|
||||
version: "0.1.0",
|
||||
activate: (app: GeoLibreAppAPI) => {
|
||||
app.setBasemap("https://example.com/style.json");
|
||||
},
|
||||
deactivate: () => {},
|
||||
};
|
||||
```
|
||||
|
||||
2. Export it from `packages/plugins/src/index.ts`.
|
||||
|
||||
```typescript
|
||||
export { myPlugin } from "./plugins/my-plugin";
|
||||
```
|
||||
|
||||
3. Register it in `apps/geolibre-desktop/src/hooks/usePlugins.ts`.
|
||||
|
||||
```typescript
|
||||
import { myPlugin } from "@geolibre/plugins";
|
||||
|
||||
manager.registerAll([
|
||||
maplibreLayerControlPlugin,
|
||||
maplibreGeoAgentPlugin,
|
||||
maplibreGeoEditorPlugin,
|
||||
myPlugin,
|
||||
]);
|
||||
```
|
||||
|
||||
Plugins can use the app API to change basemaps, add GeoJSON layers, or attach MapLibre controls. For a MapLibre control plugin, add the package dependency, import its CSS in `apps/geolibre-desktop/src/main.tsx`, then call `app.addMapControl(control, "top-left")` in `activate()` and `app.removeMapControl(control)` in `deactivate()`.
|
||||
|
||||
Built-in MapLibre controls such as Navigation, Fullscreen, Geolocate, Globe, Terrain, Scale, Attribution, and Logo are toggled from the desktop app's Controls menu. The same menu also opens Search, a standalone place search panel backed by the Components plugin. Keep project-specific controls such as Layer Control and Components in the plugin menu when they use the plugin API or need plugin lifecycle behavior.
|
||||
|
||||
The Components plugin wraps `maplibre-gl-components` controls and wires their layer events into the GeoLibre store. It provides Add Data shortcuts for FlatGeobuf, PMTiles, Zarr, LiDAR, and Gaussian splats, while raster COG and GeoTIFF layers can also be added through the standard Add Raster Layer dialog.
|
||||
|
||||
If a third-party MapLibre control needs app-specific styling fixes, add scoped overrides in `apps/geolibre-desktop/src/index.css` instead of editing files in `node_modules`. Keep selectors limited to the plugin control class. For example, GeoEditor toolbar buttons need a local override because MapLibre's default control button CSS can override their flex centering:
|
||||
|
||||
```css
|
||||
.geo-editor-control .geo-editor-tool-button {
|
||||
align-items: center;
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
line-height: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.geo-editor-control .geo-editor-tool-button svg {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
}
|
||||
```
|
||||
|
||||
Run checks before submitting changes:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pre-commit run --all-files
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Full documentation, including the User Guide and Tutorials, is published at
|
||||
**[geolibre.app](https://geolibre.app)**.
|
||||
|
||||
- **User Guide** - a [feature-by-feature reference](https://geolibre.app/user-guide/interface/) for the interface, adding data, layers, styling, the attribute table, map controls, processing, the SQL Workspace, data integrations, plugins, settings, and embedding.
|
||||
- **Tutorials** - [hands-on, end-to-end workflows](https://geolibre.app/tutorials/): your first map, cloud-native data, vector analysis, terrain analysis, spatial SQL, and sharing and embedding.
|
||||
- **Reference**
|
||||
- [Architecture](docs/architecture.md)
|
||||
- [Android](docs/android.md)
|
||||
- [Project format](docs/project-format.md)
|
||||
- [Plugin API](docs/plugin-api.md)
|
||||
- [UI Profiles](docs/ui-profiles.md)
|
||||
- [Python package (Jupyter)](docs/python.md)
|
||||
- [Roadmap](docs/roadmap.md)
|
||||
- [How to Cite](docs/citation.md)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
GeoLibre is built on the free and open-source geospatial and web communities — including MapLibre GL JS, deck.gl, DuckDB-WASM Spatial, Turf.js, Tauri, React, and many more. See the full [Acknowledgements](https://geolibre.app/acknowledgements/) page for the complete list of projects and community contributors.
|
||||
|
||||
- The **Atmosphere Effects** plugin (deep-space backdrop, parallax starfield, comets, and the globe atmosphere halo) adapts the technique and visual design from [Leonel Dias](https://leoneljdias.github.io/)'s article [*Globe atmosphere, halo, and comets*](https://leoneljdias.github.io/posts/globe-atmosphere-halo-comets/) — the layered Canvas 2D approach, the halo gradient and "screen" blend, the limb-sampling that keeps the halo aligned under pitch, and the starfield/comet parameters.
|
||||
- **Community contributors** — thanks to [**Ryanphoenix**](https://github.com/Ryanphoenix) for many valued contributions, including issue reports, feedback, and improvements.
|
||||
- **Beta testers** — thanks to [**René van der Velde**](https://github.com/renevandervelde) (Netherlands) for early testing, detailed bug reports, and feature requests.
|
||||
|
||||
## Citation
|
||||
|
||||
If you use GeoLibre in your work, please cite it. GeoLibre is archived on [Zenodo](https://zenodo.org/), which mints a DOI for every release. The concept DOI below always resolves to the latest version.
|
||||
|
||||
[](https://doi.org/10.5281/zenodo.20785400)
|
||||
|
||||
> Wu, Q. (2026). GeoLibre: A lightweight, cloud-native GIS platform for visualizing, exploring, and analyzing geospatial data. Zenodo. <https://doi.org/10.5281/zenodo.20785400>
|
||||
|
||||
You can also use GitHub's **"Cite this repository"** button (which reads [`CITATION.cff`](CITATION.cff)) to copy a ready-made APA or BibTeX entry. See the [How to Cite](https://geolibre.app/citation/) page for more formats.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`opengeos/GeoLibre`
|
||||
- 原始仓库:https://github.com/opengeos/GeoLibre
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
@@ -0,0 +1,31 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="%BASE_URL%favicon.png" sizes="32x32" />
|
||||
<link rel="icon" href="%BASE_URL%favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" href="%BASE_URL%apple-touch-icon.png" />
|
||||
<!-- viewport-fit=cover lets the page extend under the mobile status/nav bars
|
||||
and display cutouts so env(safe-area-inset-*) report their sizes; the app
|
||||
shell then pads itself by those insets (see #root in src/index.css). -->
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="A free and open-source, lightweight, cloud-native GIS platform for visualizing, exploring, and analyzing geospatial data, running in the browser, on the desktop, on mobile, and inside Jupyter notebooks while keeping your data local and private."
|
||||
/>
|
||||
<meta name="theme-color" content="#2f8f85" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<!-- Apple-prefixed variant kept for older iOS Safari (pre-17.4) home-screen
|
||||
standalone mode; the standard tag above is what newer browsers expect. -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-title" content="GeoLibre" />
|
||||
<title>GeoLibre</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"jupyter-lite-schema-version": 0,
|
||||
"jupyter-config-data": {
|
||||
"exposeAppInBrowser": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"LiteBuildConfig": {
|
||||
"apps": ["lab", "tree", "notebooks", "repl"],
|
||||
"no_unused_shared_packages": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Build-time dependencies for the self-hosted JupyterLite site embedded in the
|
||||
# GeoLibre web build (see scripts/build-jupyterlite.mjs). These are NOT runtime
|
||||
# app dependencies — they are only needed by whoever runs `npm run
|
||||
# build:jupyterlite` to regenerate apps/geolibre-desktop/public/jupyterlite/.
|
||||
#
|
||||
# The in-browser Python kernel itself ships as WASM via jupyterlite-pyodide-kernel
|
||||
# and pulls Pyodide (numpy, pandas, …) at runtime from a CDN; geopandas/shapely
|
||||
# install on demand with `%pip install ...` / piplite.
|
||||
jupyterlite-core>=0.4,<0.6
|
||||
jupyterlite-pyodide-kernel>=0.4,<0.6
|
||||
jupyterlab>=4.2,<5
|
||||
notebook>=7.2,<8
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"name": "geolibre-desktop",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"predev": "node ../../scripts/build-jupyterlite.mjs --if-missing",
|
||||
"prebuild": "node ../../scripts/build-jupyterlite.mjs",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "node ../../scripts/tauri-build.mjs",
|
||||
"tauri:build:native-duckdb": "node ../../scripts/tauri-build.mjs --native-duckdb"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.110.0",
|
||||
"@carbonplan/zarr-layer": "^0.6.0",
|
||||
"@cereusdb/standard": "^0.2.0",
|
||||
"@deck.gl/aggregation-layers": "9.3.6",
|
||||
"@deck.gl/core": "^9.3.6",
|
||||
"@deck.gl/geo-layers": "^9.3.6",
|
||||
"@deck.gl/layers": "^9.3.6",
|
||||
"@deck.gl/mapbox": "^9.3.6",
|
||||
"@deck.gl/mesh-layers": "^9.3.6",
|
||||
"@developmentseed/deck.gl-geotiff": "^0.7.0",
|
||||
"@developmentseed/deck.gl-raster": "^0.7.0",
|
||||
"@duckdb/duckdb-wasm": "1.33.1-dev57.0",
|
||||
"@dvt3d/maplibre-three-plugin": "^1.6.0",
|
||||
"@electric-sql/pglite": "^0.5.4",
|
||||
"@electric-sql/pglite-postgis": "^0.2.4",
|
||||
"@geolibre/core": "*",
|
||||
"@geolibre/map": "*",
|
||||
"@geolibre/plugins": "*",
|
||||
"@geolibre/processing": "*",
|
||||
"@geolibre/ui": "*",
|
||||
"@geoman-io/maplibre-geoman-free": "^0.8.4",
|
||||
"@google/genai": "^2.10.0",
|
||||
"@mapbox/mapbox-gl-rtl-text": "^0.4.0",
|
||||
"@modelcontextprotocol/sdk": "^1.25.2",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@osmix/core": "^0.1.10",
|
||||
"@osmix/geojson": "^0.0.15",
|
||||
"@osmix/pbf": "^0.0.9",
|
||||
"@strands-agents/sdk": "^1.7.0",
|
||||
"@tanstack/react-virtual": "^3.14.5",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||
"@tauri-apps/plugin-fs": "^2.5.1",
|
||||
"@tauri-apps/plugin-http": "^2.5.2",
|
||||
"@tauri-apps/plugin-opener": "^2.5.4",
|
||||
"apache-arrow": "^21.1.0",
|
||||
"cesium": "^1.143.0",
|
||||
"dompurify": "^3.4.10",
|
||||
"exifr": "^7.1.3",
|
||||
"fflate": "^0.8.3",
|
||||
"gdal3.js": "^2.8.1",
|
||||
"i18next": "^26.3.4",
|
||||
"jspdf": "^4.2.1",
|
||||
"mapillary-js": "^4.1.2",
|
||||
"maplibre-gl": "^5.24.0",
|
||||
"maplibre-gl-3d-tiles": "^0.5.4",
|
||||
"maplibre-gl-basemap-control": "^0.12.0",
|
||||
"maplibre-gl-components": "^0.25.4",
|
||||
"maplibre-gl-duckdb": "^0.2.3",
|
||||
"maplibre-gl-earth-engine": "^0.4.2",
|
||||
"maplibre-gl-enviroatlas": "^0.1.1",
|
||||
"maplibre-gl-esri-wayback": "^0.2.2",
|
||||
"maplibre-gl-fema-wms": "^0.1.2",
|
||||
"maplibre-gl-geo-editor": "^0.10.1",
|
||||
"maplibre-gl-geoagent": "^0.5.4",
|
||||
"maplibre-gl-lidar": "^0.16.2",
|
||||
"maplibre-gl-nasa-earthdata": "^0.1.4",
|
||||
"maplibre-gl-national-map": "^0.1.1",
|
||||
"maplibre-gl-planetary-computer": "^0.3.0",
|
||||
"maplibre-gl-raster": "^0.10.1",
|
||||
"maplibre-gl-splat": "^0.2.8",
|
||||
"maplibre-gl-streetview": "^0.7.0",
|
||||
"maplibre-gl-swipe": "^0.10.1",
|
||||
"maplibre-gl-time-slider": "^1.1.0",
|
||||
"maplibre-gl-usgs-lidar": "^0.11.1",
|
||||
"maplibre-gl-vector": "^0.9.2",
|
||||
"openai": "^6.39.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"react-i18next": "^17.0.8",
|
||||
"shpjs": "^6.2.0",
|
||||
"sql.js": "^1.14.0",
|
||||
"three": "^0.185.1",
|
||||
"zod": "^4.4.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.3.2",
|
||||
"@tauri-apps/cli": "^2.11.4",
|
||||
"@types/geojson": "^7946.0.16",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/sql.js": "^1.4.9",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"esbuild": "^0.28.0",
|
||||
"osmix": "^0.2.2",
|
||||
"postcss": "^8.5.16",
|
||||
"tailwindcss": "^4.3.0",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "^8.1.3",
|
||||
"vite-plugin-pwa": "^1.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 125 KiB |
@@ -0,0 +1,6 @@
|
||||
# Plugins baked into the build are dropped here as public/plugins/<id>/.
|
||||
# The plugin bundles themselves are not committed (they may be private and are
|
||||
# copied in at build/deploy time); only this folder's convention docs are.
|
||||
*
|
||||
!.gitignore
|
||||
!README.md
|
||||
@@ -0,0 +1,44 @@
|
||||
# Bundled plugins (drop-in folder)
|
||||
|
||||
Drop a plugin here to **bake it into the GeoLibre build**. It loads
|
||||
automatically on startup with no Settings entry and no manifest URL — on both
|
||||
the **web** build and the **desktop** build (the desktop app ships the same
|
||||
frontend, so one folder serves both).
|
||||
|
||||
## Layout
|
||||
|
||||
One folder per plugin, named by its plugin id, with a `plugin.json` at its root
|
||||
(the exact same content a manifest URL would serve):
|
||||
|
||||
```text
|
||||
public/plugins/
|
||||
my-plugin/
|
||||
plugin.json # { "id", "name", "version", "entry", "style"? }
|
||||
dist/
|
||||
index.js # the "entry" referenced by plugin.json
|
||||
style.css # the optional "style" referenced by plugin.json
|
||||
```
|
||||
|
||||
`entry` and `style` in `plugin.json` are resolved relative to the manifest, so
|
||||
keep them inside the plugin's own folder.
|
||||
|
||||
## How it works
|
||||
|
||||
The `bundledPlugins()` Vite plugin
|
||||
(`apps/geolibre-desktop/vite-plugins/bundled-plugins.ts`) scans this directory
|
||||
at build and dev-server start and exposes the discovered manifest paths via the
|
||||
`virtual:bundled-plugins` module. `usePlugins.ts` turns those into origin-absolute
|
||||
URLs and loads them through the normal external-plugin path (fetch → blob import
|
||||
→ register). Adding or removing a plugin is just adding or removing a folder —
|
||||
no code changes.
|
||||
|
||||
Discovery happens at **build time**, so a dev server or production build must be
|
||||
(re)started after adding, updating, or removing a plugin folder.
|
||||
|
||||
## Private plugins
|
||||
|
||||
The bundles are **git-ignored** (see `.gitignore`) so private plugin code stays
|
||||
out of this repo's history. Copy the plugin folder in at build/deploy time (for
|
||||
example, in CI before `npm run build`, or with a plugin repo's own install
|
||||
script). The discovery code is generic and committed; only the plugin payload
|
||||
is excluded.
|
||||
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 145 KiB |
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* GeoLibre Pyodide vector worker (classic Web Worker).
|
||||
*
|
||||
* Runs the GeoPandas/Shapely vector tools entirely in the browser. This is a
|
||||
* plain JS file served from public/ (NOT bundled by Vite) on purpose: Pyodide
|
||||
* 0.27's loader only supports a classic worker (importScripts) or the main
|
||||
* thread — a module worker hits its "Cannot determine runtime environment"
|
||||
* error. Loading the CDN UMD via importScripts also avoids bundling
|
||||
* pyodide.mjs's dynamic node: imports.
|
||||
*
|
||||
* Protocol (postMessage):
|
||||
* in: { type: "init", indexURL, vectorOpsSource }
|
||||
* { type: "run", id, request } // request = {tool_id, geojson, overlay, parameters}
|
||||
* out: { type: "progress", phase }
|
||||
* { type: "ready" }
|
||||
* { type: "result", id, geojson, messages }
|
||||
* { type: "error", id, message } // id omitted for init failures
|
||||
*
|
||||
* The main thread (pyodide-vector-loader.ts) reads the Python source via a Vite
|
||||
* ?raw import and hands it over in the init message, so the single
|
||||
* source-of-truth backend module is what runs here.
|
||||
*/
|
||||
|
||||
let readyPromise = null;
|
||||
let pyodide = null;
|
||||
|
||||
async function initialize(indexURL, vectorOpsSource) {
|
||||
self.postMessage({ type: "progress", phase: "Downloading Python runtime" });
|
||||
// The UMD build attaches loadPyodide to the worker global scope.
|
||||
// Trust note: importScripts has no SRI, and Pyodide loads its own
|
||||
// pyodide.asm.js/WASM from indexURL internally, so verifying this entry
|
||||
// script alone would not secure the runtime. The default CDN therefore
|
||||
// carries a trust assumption; self-host via VITE_PYODIDE_INDEX_URL to remove
|
||||
// the external dependency (see docs/user-guide/processing.md).
|
||||
self.importScripts(`${indexURL}pyodide.js`);
|
||||
pyodide = await self.loadPyodide({ indexURL });
|
||||
|
||||
self.postMessage({ type: "progress", phase: "Loading GeoPandas" });
|
||||
// geopandas pulls shapely, pyproj, pandas, numpy, fiona transitively.
|
||||
await pyodide.loadPackage("geopandas");
|
||||
|
||||
// Define run_vector_tool / run_vector_tool_json (the shared backend module).
|
||||
pyodide.runPython(vectorOpsSource);
|
||||
self.postMessage({ type: "ready" });
|
||||
}
|
||||
|
||||
self.onmessage = async (event) => {
|
||||
const data = event.data || {};
|
||||
|
||||
if (data.type === "init") {
|
||||
readyPromise ??= initialize(data.indexURL, data.vectorOpsSource).catch(
|
||||
(err) => {
|
||||
// Reset so a later init can retry after a transient failure.
|
||||
readyPromise = null;
|
||||
self.postMessage({
|
||||
type: "error",
|
||||
message:
|
||||
err && err.message ? err.message : "Failed to load Python runtime",
|
||||
});
|
||||
// Do not re-throw: the posted "error" message is the sole error
|
||||
// channel, and re-throwing would reject this catch() promise with no
|
||||
// live awaiter (readyPromise is already null) — an unhandled rejection.
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === "run") {
|
||||
const { id, request } = data;
|
||||
try {
|
||||
if (!readyPromise) throw new Error("Pyodide worker not initialized");
|
||||
await readyPromise;
|
||||
// A run that was awaiting readyPromise when init failed lands here with no
|
||||
// runtime; surface a clean error rather than a null dereference below.
|
||||
if (!pyodide) throw new Error("Python runtime is not available");
|
||||
// JSON-string boundary: avoids PyProxy lifetime management and matches the
|
||||
// sidecar's JSON contract exactly.
|
||||
const fn = pyodide.globals.get("run_vector_tool_json");
|
||||
// fn is a PyProxy and must be destroyed even if the call throws (e.g. a
|
||||
// GeoPandas ValueError), or it leaks the underlying Python object. Use
|
||||
// optional chaining so a missing global doesn't mask the original error.
|
||||
let out;
|
||||
try {
|
||||
out = fn(JSON.stringify(request));
|
||||
} finally {
|
||||
fn?.destroy();
|
||||
}
|
||||
const parsed = JSON.parse(out);
|
||||
self.postMessage({
|
||||
type: "result",
|
||||
id,
|
||||
geojson: parsed.geojson,
|
||||
messages: parsed.messages,
|
||||
});
|
||||
} catch (err) {
|
||||
self.postMessage({
|
||||
type: "error",
|
||||
id,
|
||||
message: err && err.message ? err.message : String(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
[package]
|
||||
name = "geolibre-desktop"
|
||||
version = "2.0.0"
|
||||
description = "GeoLibre Desktop — lightweight cloud-native desktop GIS"
|
||||
authors = ["GeoLibre Contributors"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "geolibre_desktop_lib"
|
||||
crate-type = ["lib", "cdylib", "staticlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
native-duckdb = ["dep:duckdb"]
|
||||
|
||||
# Optimize the release binary for size. The frontend assets dominate the binary,
|
||||
# but these still trim the Rust code: LTO + a single codegen unit let the linker
|
||||
# drop dead code across crates, `strip` removes symbols/debuginfo, `opt-level="s"`
|
||||
# favors size while keeping reasonable runtime speed, and `panic="abort"` drops
|
||||
# unwinding tables (a panic in the desktop shell terminates the app either way).
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
panic = "abort"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-http = "2"
|
||||
tauri-plugin-opener = "2"
|
||||
# Persists the fs scope the folder picker grants, so Browser-panel pinned
|
||||
# folders stay readable across app restarts (no re-pick required).
|
||||
tauri-plugin-persisted-scope = "2"
|
||||
duckdb = { version = "1.10504.0", features = ["bundled", "parquet"], optional = true }
|
||||
chrono = { version = "0.4", default-features = false, features = ["std"] }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
|
||||
flate2 = "1"
|
||||
tar = "0.4"
|
||||
zip = { version = "6", default-features = false, features = ["deflate"] }
|
||||
rusqlite = { version = "0.40", features = ["bundled"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# OS CSPRNG for the per-launch Jupyter token (already an indirect dep).
|
||||
getrandom = "0.4"
|
||||
|
||||
# Runtime WebKitGTK version check (already an indirect dep via tauri/wry).
|
||||
[target."cfg(target_os = \"linux\")".dependencies]
|
||||
webkit2gtk-sys = "2"
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"identifier": "default",
|
||||
"description": "Default capabilities for GeoLibre Desktop",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"dialog:default",
|
||||
"fs:default",
|
||||
"fs:allow-read-dir",
|
||||
"fs:allow-read-file",
|
||||
"fs:allow-read-text-file",
|
||||
"fs:allow-write-file",
|
||||
"fs:allow-write-text-file",
|
||||
{
|
||||
"identifier": "opener:allow-open-url",
|
||||
"allow": [
|
||||
{ "url": "http://*" },
|
||||
{ "url": "https://*" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "Hosts routed through the native HTTP client to bypass the WebView's CORS. Geocoding (place search / reverse geocode): public Nominatim's CDN intermittently omits the CORS header on cached responses (rejected as 'Search failed'), and the native client can send the User-Agent Nominatim's policy requires — keep the geocoder hosts in sync with the GEOCODING_PROVIDERS registry / lib/geocoding-fetch.ts. share.geolibre.app (project Share + gallery): its CORS policy allows the web origin but not the Tauri WebView origin, so browser fetches fail as 'Could not reach share.geolibre.app' — see lib/share-fetch.ts. Any other host (custom/self-hosted geocoder, third-party project URL) falls back to the browser fetch.",
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{ "url": "https://nominatim.openstreetmap.org/*" },
|
||||
{ "url": "https://api.geocode.earth/*" },
|
||||
{ "url": "https://geocode.arcgis.com/*" },
|
||||
{ "url": "https://api.mapbox.com/*" },
|
||||
{ "url": "https://maps.googleapis.com/*" },
|
||||
{ "url": "https://share.geolibre.app/*" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 34 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 433 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 7.7 KiB |