JMEOS

May 7, 2026 · View on GitHub

This guide covers how to use the two existing notebooks (Jupyter_AIS.ipynb and Jupyter_BerlinMod_Brussels.ipynb) and how to create new ones from scratch.


Table of Contents

  1. Environment setup
  2. rapaio-jupyter-kernel constraints
  3. Notebook: AIS Data Processing
  4. Notebook: BerlinMod Brussels
  5. Creating a new notebook
  6. Common pitfalls and solutions

1. Environment setup

Prerequisites

Starting the container

docker run -it --name jupyter-jmeos -p 8888:8888 `
  -v "C:\path\to\JMEOS-examples:/workspace" `
  eclipse-temurin:22-jdk bash

Replace C:\path\to\JMEOS-examples with your actual path to the examples repository.

Installing Jupyter inside the container

apt-get update && apt-get install -y python3 python3-pip wget
pip3 install jupyterlab --break-system-packages

Installing the Java kernel

cd /tmp
wget https://github.com/padreati/rapaio-jupyter-kernel/releases/download/2.1.0/rapaio-jupyter-kernel-2.1.0.jar
java -jar rapaio-jupyter-kernel-2.1.0.jar -i -auto
jupyter kernelspec list   # should show "rapaio-java"

Use version 2.1.0 (compiled for Java 22). Newer versions require Java 23+.

Compiling libmeos.so (inside the jupyter container)

The libmeos.so file is already available but just in case you ever need to create another one :

The native MEOS library must be compiled for Linux. This only needs to be done once.

apt-get install -y git cmake gcc g++ libgeos-dev libproj-dev libjson-c-dev libssl-dev libgsl-dev

cd /tmp
git clone --depth 1 https://github.com/MobilityDB/MobilityDB.git
cd MobilityDB
mkdir build && cd build
cmake .. -DMEOS=ON -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
cp libmeos.so /workspace/src/libmeos.so

Verify the library is correctly built:

nm -D /workspace/src/libmeos.so | grep meos_initialize_timezone
# Expected output: T meos_initialize_timezone
# T = defined (exported symbol). U = undefined (missing — rebuild needed).

Building the JMEOS fat JAR

The fat JAR (jar/JMEOS-fat.jar) is already included in the JMEOS-examples repository. It bundles all Java dependencies (jnr-ffi, etc.) into a single file, avoiding the %maven limitations of the rapaio-jupyter-kernel.

If you need to rebuild it from the latest JMEOS sources, run the following from the JMEOS repository (not the examples repository):

mvn package -pl jmeos-core -am -DskipTests

Then copy jar/JMEOS-fat.jar into the jar/ folder of the examples repository.

Starting Jupyter

cd /workspace
jupyter lab --ip=0.0.0.0 --port=8888 --allow-root --no-browser

Open http://127.0.0.1:8888.

Resuming an existing session

docker start -ai jupyter-jmeos
cd /workspace
jupyter lab --ip=0.0.0.0 --port=8888 --allow-root --no-browser

Compiling libmeos.so only needs to be done once (handled by the Dockerfile). After a kernel crash

  • manually rerun all cells before the failing cell
  • or use Run > Run All Cells to restore the full JShell state.

2. rapaio-jupyter-kernel constraints

The rapaio-jupyter-kernel executes cells as JShell snippets. Several rules differ from a standard Java class and are easy to overlook.

%jars must be alone in its cell

// WRONG — mixing %jars with code or comments crashes the kernel
%jars jar/JMEOS-fat.jar
import jnr.ffi.Pointer;
// CORRECT — %jars in its own cell, imports in the next cell
%jars jar/JMEOS-fat.jar

%maven

I've used a pre-built fat JAR loaded with %jars since I couldn't make %maven work but you could also try it out instead.

Top-level method declarations

JShell supports top-level method declarations (no surrounding class needed).

// top-level method
Pointer makeShiftedBox(int base, double shiftY) {
    CommuneBox c = communes.get(base);
    String s = String.format(java.util.Locale.US,
        "SRID=3857;STBOX X((%f %f),(%f %f))",
        c.xmin(), c.ymin() + shiftY,
        c.xmax(), c.ymax() + shiftY);
    return meos.stbox_in(s);
}

State is lost after a kernel crash

Every variable, record, and method is held in the JShell session. A kernel crash wipes everything. Use Run > Run All Cells to restore or re-run every cell before the crashing one.


3. Notebook: AIS Data Processing

File: Jupyter_AIS.ipynb
Data: ais_instants.csv (50 000+ AIS observations, 5 ships, EPSG:4326)

What it demonstrates

SectionWhat happens
1 - Read CSVParse each row into a temporal point instant and a temporal float (SOG)
2 - Assemble tripsGroup observations by MMSI → build a tgeogpoint sequence per ship
3 - Store in MobilityDBInsert assembled trips into PostgreSQL/MobilityDB via JDBC
4 - Port detectionDetect whether each AIS position falls inside a port using Brute Force and RTree

Key design choices

Interface functions

functions.functions relies on an internal loader (JarLibraryLoader) that extracts and maps libmeos.so from the JAR's resources - a mechanism that doesn't work correctly in a JShell/rapaio-jupyter-kernel environment because the expected paths don't exist in the Docker container. By defining a custom JNR-FFI interface and calling LibraryLoader.create(...).search("/workspace/src").load("meos") directly, we bypass that mechanism entirely and tell JNR-FFI exactly where the native library lives on disk. As a side benefit, the interface only declares the handful of functions the notebook actually needs, keeping it self-contained and readable.

interface MeosAIS {
    Pointer tgeogpoint_in(String wkt);          // parse geographic point WKT
    Pointer tpointinst_make(Pointer gs, long t); // create temporal instant
    Pointer tsequence_make(...);                 // build sequence from instants
    double  tpoint_length(Pointer seq);          // trajectory length in metres
    Pointer geo_from_text(String wkt, int srid); // exact geometry for intersection check
    boolean overlaps_stbox_stbox(Pointer a, Pointer b);
    Pointer rtree_create_stbox();
    void    rtree_insert(Pointer rt, Pointer box, int id);
    Pointer rtree_search(Pointer rt, Pointer box, Pointer countPtr);
    void    rtree_free(Pointer rt);
}
String MEOS_LIB_PATH = "/workspace/src";

MeosAIS meos = LibraryLoader.create(MeosAIS.class)
.search(MEOS_LIB_PATH)
.load("meos");

4. Notebook: BerlinMod Brussels

File: Jupyter_BerlinMod_Brussels.ipynb
Data: berlinmod_instants.csv (89 000 vehicle positions, EPSG:3857) and brussels_communes.csv (19 communes, EPSG:3857)

What it demonstrates

SectionWhat happens
2 - Load communesParse EWKB hex → geom_from_hexewkbgeo_to_stbox → store bbox doubles
3 - Load positionsParse SRID=3857;POINT(x y) → build point STBOX → build POINT geometry
4 - Brute Forceoverlaps_stbox_stbox over all 19 communes for every position
5 - RTreeSame query via rtree_search
6 - Exact checkgeom_intersects2d to eliminate false positives from the MBR filter
7 - Summary tableTiming + match counts for all 4 combinations
8 - Scalability benchmark19 / 100 / 500 / 2000 synthetic regions, all 4 combinations

Key design choices

Both datasets share SRID 3857 - no coordinate conversion is needed. This is the main difference from the AIS notebook where UTM→WGS84 conversion was required.

SRID on vehicle STBoxes. The STBox of a vehicle position must carry SRID=3857; so it matches the commune STBoxes (which inherit SRID 3857 from geo_to_stbox). Without the prefix, MEOS raises [MEOS ERROR 12] Operation on mixed SRID.

Scalability benchmark - tiling logic. The 19 real commune bounding boxes are tiled on a grid to simulate N > 19 regions. For index j:

base  = j % 19          → which commune to copy (cycles 0 → 18 → 0 → 18 → ...)
row   = j / 19          → grid row (integer division: 0, 0, ..., 1, 1, ..., 2, ...)
shift = row × 50 000 m  → Y offset (northward, SRID 3857 metres)

The exact check uses commune[j % 19].geom() (unshifted polygon) even for shifted boxes. Because the polygon sits at Brussels while the box is 50-150 km north, geom_intersects2d almost always returns false which correctly benchmarks the elimination cost without inflating the match count.


5. Creating a new notebook

1. Declare only the MEOS functions you need.

Copy the minimal interface pattern and add only the signatures required:

interface MeosMyNotebook {

    // Lifecycle
    void meos_initialize_timezone(String tz);
    void meos_initialize_error_handler(error_handler_fn handler);
    void meos_finalize();

    // Add only what you use:
    Pointer geo_from_text(String wkt, int srid);
    Pointer geom_from_hexewkb(String hex);
    Pointer geo_to_stbox(Pointer geom);
    double  stbox_xmin(Pointer box);
    double  stbox_ymin(Pointer box);
    double  stbox_xmax(Pointer box);
    double  stbox_ymax(Pointer box);
    boolean overlaps_stbox_stbox(Pointer a, Pointer b);
    boolean geom_intersects2d(Pointer gs1, Pointer gs2);
    Pointer stbox_in(String str);
    Pointer rtree_create_stbox();
    void    rtree_insert(Pointer rt, Pointer box, int id);
    Pointer rtree_search(Pointer rt, Pointer box, Pointer countPtr);
    void    rtree_free(Pointer rt);
}

2. Use record for data holders.

Java records (supported in JShell) are the cleanest way to bundle geometry pointers with metadata:

record RegionBox(
    int    id,
    String name,
    Pointer geom,   // polygon geometry - used in geom_intersects2d
    Pointer bbox,   // bounding STBOX - used in overlaps_stbox_stbox / rtree
    double xmin, double ymin, 
    double xmax, double ymax
) {}

record ObservationPoint(
    int    id,
    String t,       // timestamp string (for display)
    double x,
    double y,
    Pointer bbox,   // point STBOX (collapsed: min == max)
    Pointer geom    // POINT geometry - used in geom_intersects2d
) {}

3. Always clean up.

meos.meos_finalize();

Minimal skeleton ("copy-paste" template)

// Cell 1: %jars (ALONE)
%jars jar/JMEOS-fat.jar

// Cell 2: imports 
import jnr.ffi.Pointer;
import jnr.ffi.LibraryLoader;
import java.util.*;
import java.io.*;
System.out.println("Imports OK");

// Cell 3: interface + helper 
interface error_handler_fn {
    @jnr.ffi.annotations.Delegate
    void call(int level, int code, String message);
}

interface MeosMyNotebook {
    void    meos_initialize_timezone(String tz);
    void    meos_initialize_error_handler(error_handler_fn h);
    void    meos_finalize();
    Pointer geo_from_text(String wkt, int srid);
    Pointer geom_from_hexewkb(String hex);
    Pointer geo_to_stbox(Pointer geom);
    double  stbox_xmin(Pointer box);
    double  stbox_ymin(Pointer box);
    double  stbox_xmax(Pointer box);
    double  stbox_ymax(Pointer box);
    boolean overlaps_stbox_stbox(Pointer a, Pointer b);
    boolean geom_intersects2d(Pointer gs1, Pointer gs2);
    Pointer stbox_in(String str);
    Pointer rtree_create_stbox();
    void    rtree_insert(Pointer rt, Pointer box, int id);
    Pointer rtree_search(Pointer rt, Pointer box, Pointer countPtr);
    void    rtree_free(Pointer rt);
}

String ptrToString(Pointer p) {
    return p == null ? "(null)" : p.getString(0);
}
System.out.println("Interface defined");

// Cell 4: load library + initialise 
MeosMyNotebook meos = LibraryLoader.create(MeosMyNotebook.class)
    .search("/workspace/src")
    .load("meos");

error_handler_fn errHandler =
    (lvl, code, msg) -> System.err.printf("[MEOS ERROR %d] %s%n", code, msg);

meos.meos_initialize_timezone("UTC");
meos.meos_initialize_error_handler(errHandler);
System.out.println("MEOS initialised");

6. Common pitfalls and solutions

ProblemSymptomFix
%jars mixed with codeKernel crash on first cellPut %jars alone in its own cell
Missing Locale.USCoordinates contain commas, WKT rejectedAdd java.util.Locale.US to all String.format() calls
STBox SRID mismatch[MEOS ERROR 12] Operation on mixed SRIDPrefix vehicle STBoxes with SRID=3857;
stbox_out decimal issueNumberFormatException: multiple pointsUse stbox_xmin/ymin/xmax/ymax accessors instead
JNR-FFI string crashJVM crash on string-returning functionDeclare return type as Pointer, read with .getString(0)
RTree overhead > savingsRTree slower than brute forceExpected with low N; RTree wins at larger N
State lost after errorVariables undefined in later cellsRun > Run All Cells to restore full JShell session
geom_intersects2d always falseExact check never confirms matchesCheck that both geometries share the same SRID