@@ -12,42 +12,98 @@ import {
1212import {
1313 UpdateOnChangePlugin ,
1414 MVTTilesPlugin ,
15- MVTTilesMeshPlugin
15+ MVTTilesMeshPlugin ,
16+ PMTilesPlugin ,
17+ PMTilesMeshPlugin
1618} from '3d-tiles-renderer/plugins' ;
1719
1820import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js' ;
1921
2022let 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 ---
2676const 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
52108init ( ) ;
53109setupGUI ( ) ;
@@ -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+
190316function onWindowResize ( ) {
191317
192318 camera . aspect = window . innerWidth / window . innerHeight ;
0 commit comments