npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sabremaps/react-native

v0.1.1

Published

Cloud-Optimized GeoTIFFs on a MapLibre React Native map, offline and restyled at runtime: sabre's tile server runs inside the app.

Readme

@sabremaps/react-native

Cloud-Optimized GeoTIFFs on a MapLibre React Native map, from files on the device, restyled at runtime. No network, no pre-rendered tiles: sabre's tile server runs inside the app and renders each tile from the raster as MapLibre asks for it.

Android and iOS (16.4 or later; on the simulator, Apple silicon Macs only).

npx expo install @sabremaps/react-native @maplibre/maplibre-react-native

Add the config plugin to app.json, then rebuild the app (npx expo run:android, npx expo run:ios or a new dev client; Expo Go cannot load native code):

{ "expo": { "plugins": ["@maplibre/maplibre-react-native", "@sabremaps/react-native"] } }

On Expo SDK 57 built with Xcode 27, iOS apps crash at launch unless they adopt the UIScene lifecycle, whatever native modules they use. SDK 58's template does; on 57, turn it on with ["expo-build-properties", { "ios": { "enableSceneSupport": true } }].

The plugin lets the app talk plain HTTP to 127.0.0.1, and to nowhere else, so MapLibre can reach the in-app server. Bare React Native apps need Expo Modules installed.

import { Camera, Map } from "@maplibre/maplibre-react-native";
import { SabreRasterSource, useRasterInfo } from "@sabremaps/react-native";

function DemMap() {
  const { info } = useRasterInfo("dem.tif");
  const [colormap, setColormap] = useState("viridis");
  if (!info?.extent) return null;
  const { west, south, east, north } = info.extent;

  return (
    <Map mapStyle={offlineStyle}>
      <Camera initialViewState={{ bounds: [west, south, east, north] }} />
      <SabreRasterSource
        id="dem"
        source="dem.tif"
        style={{ mode: "colormap", colormap, min: info.stats_min ?? 0, max: info.stats_max ?? 3000 }}
      />
    </Map>
  );
}

Where rasters come from

Local rasters are read from one directory, the file root: the app's files directory on Android and its Documents directory on iOS by default (Paths.document in expo-file-system, on both). Put GeoTIFFs there, by downloading them, copying them out of the app bundle, or from a document picker, and name them by file name:

import { File, Paths } from "expo-file-system";
await File.downloadFileAsync("https://example.com/dem.tif", new File(Paths.document, "dem.tif"));

<SabreRasterSource id="dem" source="dem.tif" />

A source can be:

  • a file name in the file root: "dem.tif", "surveys/2026/dem.tif";
  • a file:// URI of a file inside the file root, as expo-file-system gives;
  • an https:// URL, read with range requests and cached in memory.

Anything outside the file root is refused. Cloud-Optimized GeoTIFFs (tiled, with overviews) render fastest at every zoom; a plain striped GeoTIFF works but reads more at low zooms. Compression: none, LZW, Deflate or PackBits. JPEG, ZSTD, LERC and WebP tiles are not decoded yet, so convert with gdal_translate -co COMPRESS=DEFLATE -co PREDICTOR=2.

Styles

style takes sabre's style parameters, the same names and defaults as its HTTP API and @sabremaps/browser:

| Field | Default | | | --- | --- | --- | | mode | colormap | colormap, rgb, hillshade, contour or classified | | colormap | viridis | viridis, plasma, turbo, greys, rdylbu, spectral, reds, blues, greens, ylgnbu, hot | | min, max | 0, 255 | Value range mapped across the ramp | | nodata | from the file | Values drawn transparent | | stops | | classified: [[threshold, "#rrggbb" or [r, g, b]], …] | | azimuth, altitude, z_factor | 315, 45, 1 | hillshade: light direction, elevation, exaggeration (elevations in metres) | | hillshade_colormap | | hillshade: a ramp blended under the shading | | contour_level | 10 | contour: bands between min and max | | rgb_min_r … rgb_max_b | 0 … 255 | rgb: per-channel black and white points | | interpolation | nearest | bilinear for smooth resampling | | mask | | WKT polygon in WGS84; pixels outside it are transparent | | geometry_provider, geometry_id | | Clip to geometry put in with geometries (below) instead of a mask |

Changing style redraws the visible tiles. MapLibre builds a raster source once and ignores later changes to its tiles, so the component remounts the source under the same ids; layers placed relative to it with beforeId stay put.

Clipping to fields

A mask goes in the tile URL, on every tile MapLibre asks for, and a farm's worth of fields is more than a URL can hold. Put the geometry in once and name it instead:

import { geometries, SabreRasterSource } from "@sabremaps/react-native";

await geometries.set("blocks", { 19519: blockGeoJson, 19520: "POLYGON((…))" });

<SabreRasterSource id="ndvi" source="ndvi.tif"
  style={{ colormap: "rdylgn", geometry_provider: "blocks", geometry_id: [19519, 19520] }} />

set() adds or overwrites the ids it names and leaves the rest; replace() swaps the provider's whole contents in one step, and replace(provider, {}) empties it; delete() removes ids. Geometry is WGS84, as WKT or GeoJSON text or a GeoJSON object. Several ids clip to their union, and an id that is not there is an error rather than a tile drawn without it.

Each change resolves to the provider's new revision, and a <SabreRasterSource> whose style names that provider redraws. For your own sources, useGeometryRevision(provider) gives the revision as state, and geometries.subscribe() calls back on every change. queryRaster(source, { geometry_provider, geometry_id }) gives zonal statistics for named geometry.

Geometry stays in the app's memory for as long as it runs, including when the server restarts on returning to the foreground; it is not saved to disk. The names are those of a sabre server's geometry providers, so a style that works against one works here.

API

<SabreRasterSource> is a MapLibre raster source and, unless you pass children, a raster layer ${id}-layer drawing it.

| Prop | | | --- | --- | | id | Source id | | source | File name, file:// URI or https:// URL | | style | How to draw it | | tileSize | Tile size in pixels, default 256 | | minzoom, maxzoom, attribution | As on MapLibre's RasterSource | | paint, beforeId, afterId, layerIndex | For the default layer | | children | Your own layers instead of the default one | | onError | The server could not start |

useRasterInfo(source) gives { info, error, loading }, where info has the raster's size, data type, nodata, EPSG code, WGS84 extent and center, and stats_min/stats_max when the file carries statistics.

info(source) is the same as a promise. queryRaster(source, { lng, lat }) reads the value at a point, and queryRaster(source, { polygon }) gives statistics inside a WKT polygon, or inside named geometry with { geometry_provider, geometry_id }: min, max, mean and standard deviation, and the polygon's area in m² split between pixels with data and nodata. Pixels the boundary cuts count for the part of them inside, as in exactextract. With { classes: true } it adds each distinct value's share and area, for land cover and other categorical rasters. See the server's /query documentation for every field.

geometries and useGeometryRevision(provider): see Clipping to fields.

listRasters() lists the GeoTIFFs directly inside the file root.

configure({ fileRoot, cacheBytes }) changes the file root and the memory cache for https:// sources (default 64 MB). Call it before anything else.

useEndpoint(), getEndpoint() and tileUrl(endpoint, source, style) are for building your own MapLibre sources. The endpoint can change: when the app returns to the foreground the server is checked, and restarted on a new port if the system closed it. useEndpoint() re-renders when that happens.

How it works

libsabre_mobile.so (about 4 MB on arm64) holds sabre's tile server. On first use it listens on 127.0.0.1 on a random port, under a random per-launch token that other apps on the device cannot guess, and MapLibre fetches tiles from it like any other tile URL. Tiles are sent Cache-Control: no-store, so MapLibre's disk cache does not fill with copies of tiles the device can render again.

License

Apache License 2.0, included in the package.