Proof-of-concept JOSM plugin that evals Python 3
via GraalPy (Polyglot API) and can read/write
JOSM objects (DataSet, LayerManager, MainApplication). Scripts can
import numpy.
This is not a drop-in replacement for the JOSM Scripting plugin.
GraalPy language JARs are not packed into graalpy.jar (that would strip
Truffle’s META-INF/ and the engine would not start). They ship next to it as
graalpy-libs/, listed on the plugin manifest Class-Path. JOSM puts those
URLs on PluginClassLoader. ./gradlew runJosm also puts them on
java.class.path.
install.sh copies graalpy.jar and graalpy-libs/ into JOSM's default
plugins directory. The plugin jar must keep that name — JOSM keys plugins
off the filename. Language JARs stay in the subdirectory so JOSM does not treat
them as plugins.
# After a local build (./gradlew dist → build/dist/graalpy.jar + graalpy-libs/)
./install.sh
# A GitHub release zip (graalpy.jar + graalpy-libs/ in the same folder)
unzip josm-graalpy-0.1.0.zip -d graalpy-release
./install.sh graalpy-release/graalpy.jarThe script follows JOSM's own user-data rule:
| OS | Plugins directory |
|---|---|
| Linux | ~/.josm/plugins if that legacy home still exists, otherwise ${XDG_DATA_HOME:-~/.local/share}/JOSM/plugins |
| macOS | ~/Library/JOSM/plugins |
| Windows | %APPDATA%\JOSM\plugins |
Override with --dir DIR or JOSM_PLUGIN_DIR. --libs DIR overrides the
language-JAR folder. If the plugins directory (or the JOSM user-data home
around it) does not exist, the script warns — JOSM has probably never
been started on this account — then creates the directory and copies anyway.
Restart JOSM and enable graalpy under Edit → Preferences → Plugins on
the first install. Stock java -jar josm.jar on JDK 25 needs native access
for NumPy:
java --enable-native-access=ALL-UNNAMED -jar josm.jarThe Gradle daemon stays on JDK 21 (gradle/gradle-daemon-jvm.properties)
because Gradle 8.10.2 cannot evaluate Kotlin DSL on JDK 25. Compile, test, and
runJosm use a JDK 25 toolchain (OpenJDK 25 or GraalVM 25) — runJosm
sets javaLauncher so JOSM is not started on the daemon JVM. NumPy’s C API
needs JEP 454 (JDK 22+). Compiles against JOSM 19555. The repo must have at
least one git commit (generateManifest reads HEAD).
./gradlew test # headless interop + NumPy + installed-jar classloader tests
./gradlew dist # plugin jar + graalpy-libs/ → build/dist/
./gradlew releaseZip # zip of graalpy.jar + graalpy-libs/ → build/release/
./gradlew runJosm # JOSM with the plugin (GraalPy on the classpath)
./install.sh # copy graalpy.jar and graalpy-libs/ into the real JOSM plugins dirA GitHub release is a tag v0.1.0 (no v in the plugin version itself). That
runs .github/workflows/release.yml, which builds with RELEASE_VERSION from
the tag and uploads graalpy.jar plus josm-graalpy-<version>.zip. Do not
upload the plugin jar alone: Class-Path needs graalpy-libs/ next to it.
After JOSM opens, create or download an OSM data layer, then:
- Tools → Run Python hello — adds a tagged node at 49°N, 8.4°E
- Tools → Run Python centroid — mean lat/lon of the active layer’s nodes via NumPy
- Tools → Run Python file… — eval a user-chosen
.py
Nothing is injected into the script globals. Look up JOSM the Jython way
(python.EmulateJython):
from org.openstreetmap.josm.gui import MainApplication
from org.openstreetmap.josm.data.osm import Node, DataSet
from org.openstreetmap.josm.data.coor import LatLon
import numpy as np
dataset = MainApplication.getLayerManager().getActiveDataSet()
n = Node(LatLon(49.0, 8.4)) # there is no Node(lat, lon) in JOSMjava.type("org.openstreetmap.josm…") still works. Other plugins become
visible after PluginVisibility injects their PluginClassLoader into ours
(constructor + map-frame callback). That is the inverse of lanelet2
ScriptingVisibility, which also injects into this plugin (graalpy).
With both loaded, Python 3 can
from org.openstreetmap.josm.plugins.lanelet2.api import Lanelet2Extensions
— see examples/graalpy/hello_lanelet2.py in the Lanelet2 plugin repository.
Ideas that need SciPy / Shapely / pandas / … live in inspiration/
(Tools → Run Python file…). Only NumPy is installed until you add packages.
For IDE completion while writing scripts (VS Code / IntelliJ), optional
JOSM/Java .pyi stubs live in the original Jython collection’s
java_pyi_stubs/ directory (stubPath / Pylance). They are editor-only.
NumPy is the GraalPy wheel locked in graalpy.lock (numpy==2.4.4). After
changing graalPy.packages, run ./gradlew graalPyLockPackages and commit the
lockfile. --enable-native-access=ALL-UNNAMED is required on the test / JOSM
JVM.
GPL-3.0-or-later (see LICENSE), Copyright (C) 2026 Karlsruhe Institute of Technology (KIT), Institute of Measurement and Control Systems (MRT). This plugin runs inside JOSM (licensed GPL-2.0-or-later), so it is licensed GPLv3 to stay compatible with JOSM's "or later" license.
Maintainer: Richard Schwarzkopf (schwarzkopf@fzi.de).
GraalPy Community Edition JARs in graalpy-libs/ keep their own licenses
(primarily UPL 1.0). They are not packed into graalpy.jar.