Skip to content

Commit abfe345

Browse files
committed
Add PMTiles support and refactor MVT rendering
- Add PMTilesLoaderBase for core PMTiles archive handling - Add PMTilesMeshPlugin for 3D mesh rendering from PMTiles - Add PMTilesPlugin and PMTilesImageSource for texture rendering - Refactor MVT rendering into composable parts: - VectorTileStyler: shared styling configuration - VectorTileCanvasRenderer: canvas/texture rendering - VectorTileMeshRenderer: 3D mesh rendering - VectorTileIterator: feature iteration utility
1 parent 7a74228 commit abfe345

27 files changed

Lines changed: 1555 additions & 869 deletions

example/three/geojson.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/three/mvt_globe.js

Lines changed: 200 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -12,42 +12,98 @@ import {
1212
import {
1313
UpdateOnChangePlugin,
1414
MVTTilesPlugin,
15-
MVTTilesMeshPlugin
15+
MVTTilesMeshPlugin,
16+
PMTilesPlugin,
17+
PMTilesMeshPlugin
1618
} from '3d-tiles-renderer/plugins';
1719

1820
import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
1921

2022
let scene, renderer, camera, controls, tiles, gui;
23+
let layersFolder = null;
24+
let colorsFolder = null;
25+
26+
// --- Source Presets ---
27+
// Each provider has different layer names and styling conventions
28+
const SOURCE_PRESETS = {
29+
PMTiles: {
30+
name: 'Protomaps PMTiles',
31+
url: 'https://demo-bucket.protomaps.com/v4.pmtiles',
32+
requiresApiKey: false,
33+
// Protomaps layer names (v4 basemap)
34+
layers: {
35+
water: { name: 'water', enabled: true, color: '#4a90d9' },
36+
earth: { name: 'earth', enabled: true, color: '#f2efe9' },
37+
landuse: { name: 'landuse', enabled: false, color: '#e8e4d8' },
38+
landcover: { name: 'landcover', enabled: false, color: '#d4e8c2' },
39+
natural: { name: 'natural', enabled: false, color: '#c8d9af' },
40+
roads: { name: 'roads', enabled: false, color: '#ffffff' },
41+
buildings: { name: 'buildings', enabled: false, color: '#d9d0c9' },
42+
transit: { name: 'transit', enabled: false, color: '#888888' },
43+
boundaries: { name: 'boundaries', enabled: true, color: '#ff6b6b' },
44+
places: { name: 'places', enabled: true, color: '#333333' },
45+
pois: { name: 'pois', enabled: false, color: '#7d4e24' },
46+
},
47+
defaultColor: '#cccccc'
48+
},
49+
MVT: {
50+
name: 'Mapbox Streets',
51+
urlTemplate: 'https://api.mapbox.com/v4/mapbox.mapbox-streets-v8/{z}/{x}/{y}.vector.pbf?access_token=',
52+
requiresApiKey: true,
53+
// Mapbox Streets v8 layer names
54+
layers: {
55+
water: { name: 'water', enabled: true, color: '#4a90d9' },
56+
waterway: { name: 'waterway', enabled: true, color: '#4a90d9' },
57+
landuse: { name: 'landuse', enabled: false, color: '#e8e4d8' },
58+
landuse_overlay: { name: 'landuse_overlay', enabled: false, color: '#d4e8c2' },
59+
park: { name: 'park', enabled: false, color: '#c8d9af' },
60+
natural_label: { name: 'natural_label', enabled: false, color: '#5d8a3e' },
61+
road: { name: 'road', enabled: false, color: '#ffffff' },
62+
building: { name: 'building', enabled: false, color: '#d9d0c9' },
63+
transit: { name: 'transit', enabled: false, color: '#888888' },
64+
boundaries: { name: 'admin', enabled: true, color: '#ff6b6b' },
65+
place_label: { name: 'place_label', enabled: true, color: '#333333' },
66+
poi_label: { name: 'poi_label', enabled: false, color: '#7d4e24' },
67+
},
68+
defaultColor: '#cccccc'
69+
}
70+
};
2171

22-
const apiKey = localStorage.getItem( 'mapbox_key' ) || prompt( 'Enter Mapbox API Key' );
23-
if ( apiKey ) localStorage.setItem( 'mapbox_key', apiKey );
72+
// Mapbox API key - stored in localStorage for convenience
73+
let apiKey = localStorage.getItem( 'mapbox_key' ) || '';
2474

25-
// --- Dynamic Filter State ---
75+
// --- Application State ---
2676
const state = {
27-
pluginType: 'Mesh',
28-
// Layer Toggles
29-
showWater: true,
30-
showBuildings: false,
31-
showRoads: false,
32-
showTransit: false,
33-
showLanduse: false,
34-
showAdmin: true,
35-
showLabels: true,
36-
// Property Filters
37-
maxAdminLevel: 1,
77+
sourceType: 'PMTiles',
78+
renderMode: 'Texture',
79+
// Layer visibility (populated from preset)
80+
layers: {},
81+
// Layer colors (populated from preset)
82+
colors: {},
83+
// Filter settings
3884
maxSymbolRank: 3,
39-
colors: {
40-
water: '#201f20',
41-
landuse: '#caedc1',
42-
building: '#eeeeee',
43-
road: '#444444',
44-
admin: '#ff0000',
45-
poi: '#ffcc00',
46-
default: '#222222'
47-
}
4885
};
4986

50-
const MVT_URL = `https://api.mapbox.com/v4/mapbox.mapbox-streets-v8/{z}/{x}/{y}.vector.pbf?access_token=${apiKey}`;
87+
// Initialize state from default preset
88+
function initStateFromPreset( presetName ) {
89+
90+
const preset = SOURCE_PRESETS[ presetName ];
91+
state.layers = {};
92+
state.colors = {};
93+
94+
for ( const key in preset.layers ) {
95+
96+
const layer = preset.layers[ key ];
97+
state.layers[ key ] = layer.enabled;
98+
state.colors[ layer.name ] = layer.color;
99+
100+
}
101+
102+
state.colors.default = preset.defaultColor;
103+
104+
}
105+
106+
initStateFromPreset( state.sourceType );
51107

52108
init();
53109
setupGUI();
@@ -78,36 +134,31 @@ function init() {
78134

79135
}
80136

81-
function mvtFilter( feature, layerName ) {
137+
function createFilter( preset ) {
82138

83-
const props = feature.properties;
139+
const layerNameToKey = {};
140+
for ( const key in preset.layers ) {
84141

85-
// 1. Layer Visibility Checks
86-
if ( layerName === 'water' && ! state.showWater ) return false;
87-
if ( layerName === 'building' && ! state.showBuildings ) return false;
88-
if ( layerName === 'road' && ! state.showRoads ) return false;
89-
if ( layerName === 'transit' && ! state.showTransit ) return false;
90-
if ( layerName === 'landuse' && ! state.showLanduse ) return false;
142+
layerNameToKey[ preset.layers[ key ].name ] = key;
91143

92-
// 2. Advanced Admin Filtering
93-
if ( layerName === 'admin' ) {
144+
}
94145

95-
if ( ! state.showAdmin ) return false;
96-
return props.admin_level <= state.maxAdminLevel;
146+
return function( feature, layerName ) {
97147

98-
}
148+
const key = layerNameToKey[ layerName ];
99149

100-
// 3. Label Filtering
101-
if ( layerName === 'place_label' ) {
150+
// If layer is known, check if enabled
151+
if ( key !== undefined ) {
102152

103-
if ( ! state.showLabels ) return false;
104-
return props.symbolrank <= state.maxSymbolRank;
153+
return state.layers[ key ] === true;
105154

106-
}
155+
}
107156

108-
// Default: Only return true if it's one of our toggled layers
109-
const activeLayers = [ 'water', 'building', 'road', 'transit', 'landuse' ];
110-
return activeLayers.includes( layerName ) && state[ `show${layerName.charAt( 0 ).toUpperCase() + layerName.slice( 1 )}` ];
157+
// Unknown layers: hide by default (log for debugging)
158+
console.log( 'Unknown layer:', layerName );
159+
return false;
160+
161+
};
111162

112163
}
113164

@@ -120,6 +171,29 @@ function recreateTiles() {
120171

121172
}
122173

174+
const preset = SOURCE_PRESETS[ state.sourceType ];
175+
176+
// Check if API key is needed
177+
if ( preset.requiresApiKey && ! apiKey ) {
178+
179+
apiKey = prompt( `Enter API Key for ${preset.name}:` );
180+
if ( apiKey ) {
181+
182+
localStorage.setItem( 'mapbox_key', apiKey );
183+
184+
} else {
185+
186+
// Fall back to PMTiles if no key provided
187+
state.sourceType = 'PMTiles';
188+
initStateFromPreset( 'PMTiles' );
189+
rebuildGUI();
190+
recreateTiles();
191+
return;
192+
193+
}
194+
195+
}
196+
123197
tiles = new TilesRenderer();
124198
tiles.registerPlugin( new UpdateOnChangePlugin() );
125199

@@ -128,18 +202,38 @@ function recreateTiles() {
128202
shape: 'ellipsoid',
129203
levels: 15,
130204
tileDimension: 512,
131-
url: MVT_URL,
132205
styles: state.colors,
133-
filter: mvtFilter
206+
filter: createFilter( preset )
134207
};
135208

136-
if ( state.pluginType === 'Mesh' ) {
209+
// Select plugin based on source type and render mode
210+
if ( state.sourceType === 'PMTiles' ) {
211+
212+
pluginOptions.url = preset.url;
213+
214+
if ( state.renderMode === 'Mesh' ) {
215+
216+
tiles.registerPlugin( new PMTilesMeshPlugin( pluginOptions ) );
137217

138-
tiles.registerPlugin( new MVTTilesMeshPlugin( pluginOptions ) );
218+
} else {
219+
220+
tiles.registerPlugin( new PMTilesPlugin( pluginOptions ) );
221+
222+
}
139223

140224
} else {
141225

142-
tiles.registerPlugin( new MVTTilesPlugin( pluginOptions ) );
226+
pluginOptions.url = preset.urlTemplate + apiKey;
227+
228+
if ( state.renderMode === 'Mesh' ) {
229+
230+
tiles.registerPlugin( new MVTTilesMeshPlugin( pluginOptions ) );
231+
232+
} else {
233+
234+
tiles.registerPlugin( new MVTTilesPlugin( pluginOptions ) );
235+
236+
}
143237

144238
}
145239

@@ -151,42 +245,74 @@ function recreateTiles() {
151245

152246
}
153247

154-
function setupGUI() {
248+
function rebuildGUI() {
155249

156-
gui = new GUI();
250+
// Remove old folders if they exist
251+
if ( layersFolder ) {
252+
253+
layersFolder.destroy();
254+
layersFolder = null;
255+
256+
}
257+
258+
if ( colorsFolder ) {
259+
260+
colorsFolder.destroy();
261+
colorsFolder = null;
157262

158-
// Plugin Toggle
159-
gui.add( state, 'pluginType', [ 'Mesh', 'Texture' ] ).name( 'Renderer Mode' ).onChange( recreateTiles );
263+
}
264+
265+
const preset = SOURCE_PRESETS[ state.sourceType ];
160266

161-
// Layers Folder
162-
const layers = gui.addFolder( 'Layers' );
163-
const trigger = () => recreateTiles();
267+
// Rebuild layers folder
268+
layersFolder = gui.addFolder( 'Layers' );
269+
for ( const key in preset.layers ) {
164270

165-
layers.add( state, 'showWater' ).name( 'Water' ).onChange( trigger );
166-
layers.add( state, 'showBuildings' ).name( 'Buildings' ).onChange( trigger );
167-
layers.add( state, 'showRoads' ).name( 'Roads' ).onChange( trigger );
168-
layers.add( state, 'showTransit' ).name( 'Transit' ).onChange( trigger );
169-
layers.add( state, 'showLanduse' ).name( 'Landuse' ).onChange( trigger );
170-
layers.add( state, 'showAdmin' ).name( 'Admin Borders' ).onChange( trigger );
171-
layers.add( state, 'showLabels' ).name( 'Labels' ).onChange( trigger );
271+
const layer = preset.layers[ key ];
272+
layersFolder.add( state.layers, key )
273+
.name( key.charAt( 0 ).toUpperCase() + key.slice( 1 ) )
274+
.onChange( recreateTiles );
172275

173-
// Details Folder
174-
const details = gui.addFolder( 'Filter Settings' );
175-
details.add( state, 'maxAdminLevel', 0, 4, 1 ).name( 'Admin Detail' ).onChange( trigger );
176-
details.add( state, 'maxSymbolRank', 1, 10, 1 ).name( 'Label Density' ).onChange( trigger );
276+
}
177277

178-
// Style Folder
179-
const styleFolder = gui.addFolder( 'Map Styles' );
180-
for ( let key in state.colors ) {
278+
// Rebuild colors folder
279+
colorsFolder = gui.addFolder( 'Colors' );
280+
for ( const key in preset.layers ) {
181281

182-
styleFolder.addColor( state.colors, key )
282+
const layer = preset.layers[ key ];
283+
colorsFolder.addColor( state.colors, layer.name )
183284
.name( key.charAt( 0 ).toUpperCase() + key.slice( 1 ) )
184-
.onChange( () => recreateTiles() );
285+
.onChange( recreateTiles );
185286

186287
}
187288

188289
}
189290

291+
function setupGUI() {
292+
293+
gui = new GUI();
294+
295+
// Source & Renderer Settings
296+
const sourceFolder = gui.addFolder( 'Source & Renderer' );
297+
sourceFolder.add( state, 'sourceType', Object.keys( SOURCE_PRESETS ) )
298+
.name( 'Data Source' )
299+
.onChange( ( value ) => {
300+
301+
initStateFromPreset( value );
302+
rebuildGUI();
303+
recreateTiles();
304+
305+
} );
306+
sourceFolder.add( state, 'renderMode', [ 'Mesh', 'Texture' ] )
307+
.name( 'Render Mode' )
308+
.onChange( recreateTiles );
309+
sourceFolder.open();
310+
311+
// Initial layer and color folders
312+
rebuildGUI();
313+
314+
}
315+
190316
function onWindowResize() {
191317

192318
camera.aspect = window.innerWidth / window.innerHeight;

0 commit comments

Comments
 (0)