7 — Vector Data
▶ Live demo: run this tutorial in your browser — built from src/07_vector_data.ts. Fully self-contained (no credentials).
Three related ways to put data, not just imagery, on the globe: GeoJSON layers (your own polygons/lines/points), declarative styling of features by their properties, and CZML time-dynamic data (entities that animate along tracks). Sources: samples/geo_geojson.ts, samples/geo_tile_style.ts, samples/geo_flight_tracker.ts.
Part A — GeoJSON layers#
GeoJsonLayer bakes GeoJSON into an engine mesh anchored in your GeoFrame:
extruded polygons (buildings), world-width lines (roads), and points. Load it
three ways:
import { GeoJsonLayer } from 'taos/geo/index.js';
const layer = await GeoJsonLayer.fromUrl(device, '/data/city.geojson', geo.frame, style);
// or: GeoJsonLayer.fromGeoJson(device, jsonObject, geo.frame, style)
// or: GeoJsonLayer.fromParsed(device, parsedGeoJson, geo.frame, style) // reuse across styles
The style controls how each geometry type bakes:
const layer = GeoJsonLayer.fromGeoJson(device, json, geo.frame, {
polygon: {
extrudeHeight: (p) => (typeof p?.height === 'number' ? p.height : 0), // per-feature height
color: (p) => colorOf(p, [0.65, 0.65, 0.68]), // per-feature RGB
},
point: { height: 22 },
pickable: true, // required for pick() and per-feature recoloring
polygonLabelLift: 6, // label height above the roof
});
The layer bakes to an engine Mesh; add it to the scene like any other
renderable (the sample shows the exact draw wiring). Lines and points have their
own bake options and dedicated features (GeoLineFeature for screen-constant-width
roads).
Picking and highlighting#
With pickable: true you can ray-test the polygons and recolor the hit feature:
const hit = layer.pick([origin.x, origin.y, origin.z], [dir.x, dir.y, dir.z]);
if (hit) {
console.log(hit.featureId, hit.properties, hit.position);
layer.setFeatureColor(hit.featureId, [1, 0.8, 0.2]); // highlight
}
layer.resetColors(); // restore all features
Labels#
layer.labels(...) produces label placements from the features' properties; feed
them to GeoLabelFeature:
import { GeoLabelFeature } from 'taos/geo/geo_label_feature.js'; // direct subpath
import { TonemapFeature } from 'taos/engine/index.js';
const labels = layer.labels({ textProperty: 'name', includePolygons: true });
// The feature takes any `{ labels, frame }` source via its `scene` option, and
// runs before the tonemap pass so the text composites into the lit frame.
engine.addFeatureBefore(new GeoLabelFeature({
scene: { labels, frame: geo.frame },
pixelSize: 16,
distanceFade: { near: 300, far: 1600, minOpacity: 0.1 },
}), TonemapFeature.name);
Part B — declarative styling with Cesium3DTileStyle#
Rather than hard-coding colors, describe them with a CesiumJS-compatible
expression language over each feature's properties — the same styling that
works on 3D Tiles also applies to a GeoJsonLayer.
import { Cesium3DTileStyle } from 'taos/geo/index.js';
const style = new Cesium3DTileStyle({
color: {
conditions: [
['${height} >= 140', "color('#7e3ff2')"], // tall → purple
['${height} >= 90', "color('#e23b3b')"], // mid → red
['true', "color('white')"], // else → white
],
},
show: '${height} > 100', // hide short features entirely
});
layer.applyStyle(style); // evaluates color()/show() per feature, writes vertex RGBA
The expression language supports ${property} refs, the usual arithmetic /
comparison / logical / ternary operators, and functions like color('name'|'#hex'),
rgb(...), rgba(...), hsl(...), clamp, min, max, pow, etc. A
conditions chain is evaluated top-to-bottom; the first true case wins. show
that evaluates false sets the feature's alpha to 0 (and turns on alpha-test).
geo_tile_style.ts wires this into a live
editor with presets.
Part C — time-dynamic data (CZML)#
To animate entities over time — aircraft, vehicles, satellites — use a Clock
plus position properties sampled over time. The easiest source is CZML, which
loadCzml parses into entities (each with an interpolated position) and a clock:
import { loadCzml } from 'taos/geo/index.js';
const doc = loadCzml(czmlJson); // { clock, entities, byId }
const clock = doc.clock!; // a Clock built from the document packet
Each frame, advance the clock and read every entity's interpolated ECEF position, then convert to world space and move its marker:
engine.beforeFrame((frame) => {
clock.tick(frame.dt); // advance sim time (respects multiplier + loop)
for (const entity of doc.entities) {
const ecef = entity.position?.getValueEcef(clock.currentTime);
if (!ecef) {
continue;
}
const world = geo.frame.worldFromEcefPoint(ecef);
markerFor(entity).setPosition(world.x, world.y, world.z);
}
});
A CZML position with interpolationAlgorithm: 'LAGRANGE' and several
cartographicDegrees samples gives smooth curved tracks; 'LINEAR' (the default)
gives straight segments. The Clock carries startTime/stopTime/multiplier
(sim seconds per real second) and a clockRange ('LOOP_STOP', 'CLAMPED',
'UNBOUNDED'); clock.fraction gives a [0,1] timeline position for a scrubber.
Building tracks in code#
You don't need a CZML document — you can build a SampledPositionProperty
directly:
import { Clock, SampledPositionProperty } from 'taos/geo/index.js';
const clock = new Clock({ startTime: 0, stopTime: 1800, multiplier: 120 });
const track = new SampledPositionProperty();
track.algorithm = 'LAGRANGE';
track.degree = 2;
track.addSampleLonLat(0, -122.30, 37.62, 0);
track.addSampleLonLat(600, -122.10, 37.80, 3000);
track.addSampleLonLat(1800, -121.90, 38.05, 10000);
// each frame: track.getValueEcef(clock.currentTime) → worldFromEcefPoint → place
geo_flight_tracker.ts animates three aircraft along sampled tracks with paths, points, and labels.
Part D — styling streamed vector tiles (MapLibre GL)#
Parts A–B styled your own GeoJSON / 3D-Tiles features. The streamed OSM
vector tiles from tutorial 3 (addVectorTiles) have their own
styling path: a VectorStyle that says which OpenMapTiles source-layers render
and their fill colors, road widths, and building extrusion heights. Pass it as
opts.style:
import { parseMapLibreStyle, type VectorStyle } from 'taos/geo/index.js';
// Hand-author a VectorStyle…
const blueprint: VectorStyle = {
fill: [{ sourceLayer: 'water', color: [0.02, 0.06, 0.12] }],
line: [{ sourceLayer: 'transportation', color: [0.2, 0.85, 1.0], width: 8, filter: ['==', 'class', 'primary'] }],
extrusion: [{ sourceLayer: 'building', color: [0.1, 0.45, 0.62], heightProp: 'render_height' }],
};
geo.addVectorTiles(OPENFREEMAP_VECTOR, { style: blueprint });
// …or adapt a real MapLibre GL style document:
const parsed = parseMapLibreStyle(mapLibreStyleJson); // { style, sourceUrl? }
geo.addVectorTiles(OPENFREEMAP_VECTOR, { style: parsed.style });
parseMapLibreStyle is a best-effort adapter: it reads the subset of a MapLibre
GL style that maps onto 3D geometry — fill polygons (draped), line ribbons
(widened to meters), and fill-extrusion (buildings) — pulling each layer's
source-layer, a constant paint color, line width, extrusion height, and feature
filter. Paint expressions it can't cheaply evaluate, symbols, raster, and
hillshade are skipped; it's not a conformant MapLibre renderer. Omit style
entirely for the built-in default look (DEFAULT_VECTOR_STYLE).
To restyle live, geo.remove(vectorTileset) and addVectorTiles again with the
new style — the terrain underneath stays put.
geo_vector_style.ts swaps between
hand-authored and parsed styles over keyless OpenFreeMap tiles.
A style's symbol layers become map labels (place names, street names, POIs);
GeoSceneextracts them and exposes them as a label source — feedgeostraight to aGeoLabelFeatureto render them (see tutorial 4).
Next#
- Tutorial 8 — Projection Textures: cast an image
onto the streamed terrain and buildings with a
Projector.