From 8c51581c0bdcb70da6c4a68820612ae4848f2e93 Mon Sep 17 00:00:00 2001 From: gchoqueux Date: Thu, 23 Aug 2018 16:11:16 +0200 Subject: [PATCH 1/5] feat(protocol): add Source Add new object source: - Source have the properties to fetch the data to display - Remove all source abstractions from provider --- examples/cubic_planar.html | 58 ++--- examples/globe_vector.html | 51 +++-- examples/globe_vector_tiles.html | 30 ++- examples/globe_wfs_extruded.html | 110 ++++++---- examples/js/FeatureToolTip.js | 4 +- .../layers/JSONLayers/Administrative.json | 18 +- examples/layers/JSONLayers/Cada.json | 34 ++- examples/layers/JSONLayers/DARK.json | 10 +- examples/layers/JSONLayers/Denomination.json | 18 +- examples/layers/JSONLayers/IGN_MNT.json | 8 +- .../layers/JSONLayers/IGN_MNT_HIGHRES.json | 8 +- examples/layers/JSONLayers/OPENSM.json | 9 +- examples/layers/JSONLayers/Ortho.json | 20 +- examples/layers/JSONLayers/OrthosCRS.json | 15 +- examples/layers/JSONLayers/Railways.json | 18 +- examples/layers/JSONLayers/Region.json | 48 ++--- examples/layers/JSONLayers/ScanEX.json | 169 +++++++-------- examples/layers/JSONLayers/Transport.json | 18 +- examples/layers/JSONLayers/WORLD_DTM.json | 8 +- examples/orthographic.html | 28 ++- examples/panorama.html | 11 +- examples/planar.html | 54 +++-- examples/planar_vector.html | 91 ++++---- examples/planar_vector_tiles.html | 25 ++- examples/syncCameras.html | 16 +- examples/wfs.html | 108 +++++----- jsdoc-config.json | 9 + src/Core/Geographic/Extent.js | 55 +++-- src/Core/Prefab/Globe/GlobeLayer.js | 12 +- src/Core/Prefab/GlobeView.js | 4 +- src/Core/Prefab/Panorama/PanoramaLayer.js | 37 ++-- src/Core/Prefab/Planar/PlanarLayer.js | 8 +- src/Core/Scheduler/Scheduler.js | 23 +- src/Core/TileMesh.js | 52 ++--- src/Core/View.js | 108 ++++------ src/Layer/ColorLayer.js | 27 ++- src/Layer/ElevationLayer.js | 22 +- src/Layer/GeometryLayer.js | 17 +- src/Layer/Layer.js | 5 +- src/Layer/LayerUpdateStrategy.js | 4 +- src/Layer/TiledGeometryLayer.js | 47 +++- src/Main.js | 2 +- src/Parser/GeoJsonParser.js | 1 + src/Parser/VectorTileParser.js | 28 ++- src/Parser/XbilParser.js | 57 ++--- src/Parser/convertToTile.js | 77 +++++++ src/Parser/textureConverter.js | 47 ++++ src/Process/FeatureProcessing.js | 48 ++++- src/Process/LayeredMaterialNodeProcessing.js | 120 +++++++---- src/Process/TiledNodeProcessing.js | 46 ++-- src/Provider/DataSourceProvider.js | 131 ++++++++++++ src/Provider/Fetcher.js | 25 ++- src/Provider/OGCWebServiceHelper.js | 58 +---- src/Provider/RasterProvider.js | 130 ----------- src/Provider/StaticProvider.js | 182 ---------------- src/Provider/TMSProvider.js | 82 ------- src/Provider/TileProvider.js | 119 ++--------- src/Provider/WFSProvider.js | 94 -------- src/Provider/WMSProvider.js | 156 -------------- src/Provider/WMTSProvider.js | 201 ------------------ src/Renderer/LayeredMaterial.js | 22 +- src/Renderer/ThreeExtended/Feature2Texture.js | 6 +- src/Source/FileSource.js | 127 +++++++++++ src/Source/Source.js | 98 +++++++++ src/Source/StaticSource.js | 132 ++++++++++++ src/Source/TMSSource.js | 68 ++++++ src/Source/WFSSource.js | 118 ++++++++++ src/Source/WMSSource.js | 100 +++++++++ src/Source/WMTSSource.js | 102 +++++++++ test/unit/layeredmaterial.js | 1 + test/unit/layeredmaterialnodeprocessing.js | 37 +++- test/unit/vectortiles.js | 8 +- 72 files changed, 1994 insertions(+), 1846 deletions(-) create mode 100644 src/Parser/convertToTile.js create mode 100644 src/Parser/textureConverter.js create mode 100644 src/Provider/DataSourceProvider.js delete mode 100644 src/Provider/RasterProvider.js delete mode 100644 src/Provider/StaticProvider.js delete mode 100644 src/Provider/TMSProvider.js delete mode 100644 src/Provider/WFSProvider.js delete mode 100644 src/Provider/WMSProvider.js delete mode 100644 src/Provider/WMTSProvider.js create mode 100644 src/Source/FileSource.js create mode 100644 src/Source/Source.js create mode 100644 src/Source/StaticSource.js create mode 100644 src/Source/TMSSource.js create mode 100644 src/Source/WFSSource.js create mode 100644 src/Source/WMSSource.js create mode 100644 src/Source/WMTSSource.js diff --git a/examples/cubic_planar.html b/examples/cubic_planar.html index 5a0d72db86..6be4008fbd 100644 --- a/examples/cubic_planar.html +++ b/examples/cubic_planar.html @@ -30,6 +30,7 @@ var obj; var offset; var tileLayer; + var config; var wmsLayers = [ 'fpc_fond_plan_communaut.fpcilot', @@ -108,42 +109,49 @@ parent.add(obj); obj.updateMatrixWorld(true); - tileLayer = itowns.createPlanarLayer('planar' + wms + index, extent, { object3d: obj }); - tileLayer.disableSkirt = true; + config = { + object3d: obj, + // Since the elevation layer use color textures, specify min/max z + materialOptions: { + useColorTextureElevation: true, + colorTextureElevationMinZ: -600, + colorTextureElevationMaxZ: 400, + }, + disableSkirt: true, + }; + + tileLayer = itowns.createPlanarLayer('planar' + wms + index, extent, config); view.addLayer(tileLayer); view.addLayer({ - url: 'https://download.data.grandlyon.com/wms/grandlyon', - networkOptions: { crossOrigin: 'anonymous' }, type: 'color', - protocol: 'wms', - version: '1.3.0', id: 'wms_imagery' + wms + index, - name: wms, - projection: 'EPSG:3946', - format: 'image/jpeg', + source: { + protocol: 'wms', + url: 'https://download.data.grandlyon.com/wms/grandlyon', + version: '1.3.0', + name: wms, + projection: 'EPSG:3946', + format: 'image/jpeg', + extent, + }, }, tileLayer); view.addLayer({ - url: 'https://download.data.grandlyon.com/wms/grandlyon', - type: 'elevation', - protocol: 'wms', - networkOptions: { crossOrigin: 'anonymous' }, - version: '1.3.0', id: 'wms_elevation' + wms + index, - name: 'MNT2012_Altitude_10m_CC46', - projection: 'EPSG:3946', - heightMapWidth: 256, - format: 'image/jpeg', + type: 'elevation', + source: { + protocol: 'wms', + extent, + version: '1.3.0', + name: 'MNT2012_Altitude_10m_CC46', + projection: 'EPSG:3946', + heightMapWidth: 256, + format: 'image/jpeg', + url: 'https://download.data.grandlyon.com/wms/grandlyon', + }, }, tileLayer); - - // Since the elevation layer use color textures, specify min/max z - tileLayer.materialOptions = { - useColorTextureElevation: true, - colorTextureElevationMinZ: -600, - colorTextureElevationMaxZ: 400, - }; } // Since PlanarView doesn't create default controls, we manipulate directly three.js camera diff --git a/examples/globe_vector.html b/examples/globe_vector.html index 3c9383e3be..39b7395f2b 100644 --- a/examples/globe_vector.html +++ b/examples/globe_vector.html @@ -43,6 +43,7 @@ diff --git a/examples/globe_vector_tiles.html b/examples/globe_vector_tiles.html index 4282206fa0..d76e99c950 100644 --- a/examples/globe_vector_tiles.html +++ b/examples/globe_vector_tiles.html @@ -33,6 +33,8 @@ // define pole texture view.wgs84TileLayer.noTextureColor = new itowns.THREE.Color(0x95c1e1); + view.atmosphere.visible = false; + setupLoadingScreen(viewerDiv, view); function addLayerCb(layer) { return view.addLayer(layer); @@ -40,8 +42,9 @@ // Add two elevation layers. // These will deform iTowns globe geometry to represent terrain elevation. - promises.push(itowns.Fetcher.json('./layers/JSONLayers/WORLD_DTM.json').then(addLayerCb)); - promises.push(itowns.Fetcher.json('./layers/JSONLayers/IGN_MNT_HIGHRES.json').then(addLayerCb)); + // promises.push(itowns.Fetcher.json('./layers/JSONLayers/Ortho.json').then(addLayerCb)); + // promises.push(itowns.Fetcher.json('./layers/JSONLayers/WORLD_DTM.json').then(addLayerCb)); + // promises.push(itowns.Fetcher.json('./layers/JSONLayers/IGN_MNT_HIGHRES.json').then(addLayerCb)); // Add a vector tile layer itowns.Fetcher.json('https://raw.githubusercontent.com/Oslandia/postile-openmaptiles/master/style.json').then(function (style) { @@ -56,14 +59,21 @@ } }); + function isValidData(data, extentDestination) { + return extentDestination.zoom - data.extent.zoom < 4; + } + promises.push(view.addLayer({ type: 'color', - protocol: 'xyz', id: 'MVT', - // eslint-disable-next-line no-template-curly-in-string - url: 'https://osm.oslandia.io/data/v3/${z}/${x}/${y}.pbf', - format: 'application/x-protobuf;type=mapbox-vector', - options: { + isValidData: isValidData, + source: { + protocol: 'xyz', + // eslint-disable-next-line no-template-curly-in-string + url: 'https://osm.oslandia.io/data/v3/${z}/${x}/${y}.pbf', + format: 'application/x-protobuf;type=mapbox-vector', + projection: 'EPSG:4326', + origin: 'top', attribution: { name: 'OpenStreetMap', url: 'http://www.openstreetmap.org/', @@ -72,10 +82,7 @@ min: 2, max: 14, }, - opacity: 0.5, - }, - updateStrategy: { - type: itowns.STRATEGY_DICHOTOMY, + tileMatrixSet: 'PM', }, style: mapboxStyle, filter: mapboxFilter(supportedLayers), @@ -89,6 +96,7 @@ Promise.all(promises).then(function () { menuGlobe.addImageryLayersGUI(view.getLayers(function (l) { return l.type === 'color'; })); menuGlobe.addElevationLayersGUI(view.getLayers(function (l) { return l.type === 'elevation'; })); + // itowns.ColorLayersOrdering.moveLayerToIndex(view, 'Ortho', 0); }).catch(console.error); }); diff --git a/examples/globe_wfs_extruded.html b/examples/globe_wfs_extruded.html index 4c332d57e8..ca5bff671b 100644 --- a/examples/globe_wfs_extruded.html +++ b/examples/globe_wfs_extruded.html @@ -91,6 +91,7 @@ globeView.addLayer({ name: 'lyon_tcl_bus', + id: 'WFS Bus lines', type: 'geometry', update: itowns.FeatureProcessing.update, convert: itowns.Feature2Mesh.convert({ @@ -98,20 +99,22 @@ altitude: altitudeLine }), linewidth: 5, filter: acceptFeatureBus, - url: 'https://download.data.grandlyon.com/wfs/rdata?', - protocol: 'wfs', - version: '2.0.0', - id: 'WFS Bus lines', - typeName: 'tcl_sytral.tcllignebus', - level: 9, - projection: 'EPSG:3946', - extent: { - west: 1822174.60, - east: 1868247.07, - south: 5138876.75, - north: 5205890.19, - }, - format: 'geojson', + source: { + protocol: 'wfs', + url: 'https://download.data.grandlyon.com/wfs/rdata?', + version: '2.0.0', + typeName: 'tcl_sytral.tcllignebus', + projection: 'EPSG:3946', + extent: { + west: 1822174.60, + east: 1868247.07, + south: 5138876.75, + north: 5205890.19, + }, + zoom: { min: 9, max: 9 }, + format: 'geojson', + networkOptions: { crossOrigin: 'anonymous' }, + } }); function colorBuildings(properties) { @@ -154,6 +157,7 @@ globeView.addFrameRequester(itowns.MAIN_LOOP_EVENTS.BEFORE_RENDER, scaler); globeView.addLayer({ + id: 'WFS Buildings', type: 'geometry', update: itowns.FeatureProcessing.update, convert: itowns.Feature2Mesh.convert({ @@ -165,16 +169,23 @@ meshes.push(mesh); }, filter: acceptFeature, - url: 'http://wxs.ign.fr/72hpsel8j8nhb5qgdh07gcyp/geoportail/wfs?', - networkOptions: { crossOrigin: 'anonymous' }, - protocol: 'wfs', - version: '2.0.0', - id: 'WFS Buildings', - typeName: 'BDTOPO_BDD_WLD_WGS84G:bati_remarquable,BDTOPO_BDD_WLD_WGS84G:bati_indifferencie,BDTOPO_BDD_WLD_WGS84G:bati_industriel', - level: 14, - projection: 'EPSG:4326', - ipr: 'IGN', - format: 'application/json', + source: { + url: 'http://wxs.ign.fr/72hpsel8j8nhb5qgdh07gcyp/geoportail/wfs?', + networkOptions: { crossOrigin: 'anonymous' }, + protocol: 'wfs', + version: '2.0.0', + typeName: 'BDTOPO_BDD_WLD_WGS84G:bati_remarquable,BDTOPO_BDD_WLD_WGS84G:bati_indifferencie,BDTOPO_BDD_WLD_WGS84G:bati_industriel', + projection: 'EPSG:4326', + ipr: 'IGN', + format: 'application/json', + zoom: { min: 14, max: 14 }, + extent: { + west: 4.568, + east: 5.18, + south: 45.437, + north: 46.03, + }, + } }); function configPointMaterial(result) { @@ -217,16 +228,23 @@ size: 5, onMeshCreated: configPointMaterial, filter: selectRoad, - url: 'http://wxs.ign.fr/72hpsel8j8nhb5qgdh07gcyp/geoportail/wfs?', - networkOptions: { crossOrigin: 'anonymous' }, - protocol: 'wfs', - version: '2.0.0', - id: 'WFS Route points', - typeName: 'BDPR_BDD_FXX_LAMB93_20170911:pr', - level: 12, - projection: 'EPSG:2154', - ipr: 'IGN', - format: 'application/json', + source: { + url: 'http://wxs.ign.fr/72hpsel8j8nhb5qgdh07gcyp/geoportail/wfs?', + networkOptions: { crossOrigin: 'anonymous' }, + protocol: 'wfs', + version: '2.0.0', + id: 'WFS Route points', + typeName: 'BDPR_BDD_FXX_LAMB93_20170911:pr', + zoom: { min: 12, max: 12 }, + projection: 'EPSG:2154', + ipr: 'IGN', + format: 'application/json', + extent: new itowns.Extent('EPSG:4326', + -5.160007066832147, + 10.671941031325312, + 41.338373237911036, + 51.07519774631159).as('EPSG:2154'), + } }); var menuGlobe = new GuiTools('menuDiv', globeView); @@ -260,12 +278,12 @@ } for (let layer of globeView.getLayers()) { - if (layer.id === 'WFS Bus lines') { - layer.whenReady.then( function _(layer) { - var gui = debug.GeometryDebug.createGeometryDebugUI(menuGlobe.gui, globeView, layer); - debug.GeometryDebug.addMaterialLineWidth(gui, globeView, layer, 1, 10); - }); - } + // if (layer.id === 'WFS Bus lines') { + // layer.whenReady.then( function _(layer) { + // var gui = debug.GeometryDebug.createGeometryDebugUI(menuGlobe.gui, globeView, layer); + // debug.GeometryDebug.addMaterialLineWidth(gui, globeView, layer, 1, 10); + // }); + // } if (layer.id === 'WFS Buildings') { layer.whenReady.then( function _(layer) { var gui = debug.GeometryDebug.createGeometryDebugUI(menuGlobe.gui, globeView, layer); @@ -273,12 +291,12 @@ window.addEventListener('mousemove', picking, false); }); } - if (layer.id === 'WFS Route points') { - layer.whenReady.then( function _(layer) { - var gui = debug.GeometryDebug.createGeometryDebugUI(menuGlobe.gui, globeView, layer); - debug.GeometryDebug.addMaterialSize(gui, globeView, layer, 1, 50); - }); - } + // if (layer.id === 'WFS Route points') { + // layer.whenReady.then( function _(layer) { + // var gui = debug.GeometryDebug.createGeometryDebugUI(menuGlobe.gui, globeView, layer); + // debug.GeometryDebug.addMaterialSize(gui, globeView, layer, 1, 50); + // }); + // } } diff --git a/examples/js/FeatureToolTip.js b/examples/js/FeatureToolTip.js index 36a0c2c7aa..17c931d44d 100644 --- a/examples/js/FeatureToolTip.js +++ b/examples/js/FeatureToolTip.js @@ -2,7 +2,7 @@ // eslint-disable-next-line no-unused-vars function ToolTip(viewer, viewerDiv, tooltip, precisionPx) { var mouseDown = 0; - var layers = viewer.getLayers(function _(l) { return l.protocol === 'rasterizer'; }); + var layers = viewer.getLayers(function _(l) { return l.source && l.source.protocol === 'file'; }); document.body.onmousedown = function onmousedown() { ++mouseDown; @@ -36,7 +36,7 @@ function ToolTip(viewer, viewerDiv, tooltip, precisionPx) { for (i = 0; i < layers.length; i++) { layer = layers[i]; result = itowns.FeaturesUtils.filterFeaturesUnderCoordinate( - geoCoord, layer.feature, precision); + geoCoord, layer.source.parsedData, precision); result.sort(function compare(a, b) { return b.feature.type !== 'point'; }); for (p = 0; p < result.length; p++) { visible = true; diff --git a/examples/layers/JSONLayers/Administrative.json b/examples/layers/JSONLayers/Administrative.json index 23e9407600..b11daed33b 100644 --- a/examples/layers/JSONLayers/Administrative.json +++ b/examples/layers/JSONLayers/Administrative.json @@ -1,27 +1,23 @@ { "type": "color", - "url": "https://wxs.ign.fr/an7nvfzojv5wa96dsga5nk8w/geoportail/wmts", - "protocol": "wmts", "id": "Administrative", "title": "Administrative delimitations", "visible": false, "opacity": 1, "transparent": true, - "format": "image/png", - "updateStrategy": { - "type": 0, - "options": {} - }, - "networkOptions": { - "crossOrigin": "omit" - }, - "options": { + "source": { + "protocol": "wmts", + "url": "https://wxs.ign.fr/an7nvfzojv5wa96dsga5nk8w/geoportail/wmts", "tileMatrixSet": "PM", + "format": "image/png", "name": "ADMINISTRATIVEUNITS.BOUNDARIES", "style": "normal", "zoom": { "min": 5, "max": 20 + }, + "networkOptions": { + "crossOrigin": "omit" } } } diff --git a/examples/layers/JSONLayers/Cada.json b/examples/layers/JSONLayers/Cada.json index 2e9c29a959..56d572d736 100644 --- a/examples/layers/JSONLayers/Cada.json +++ b/examples/layers/JSONLayers/Cada.json @@ -1,21 +1,24 @@ { "type": "color", - "url": "https://wxs.ign.fr/an7nvfzojv5wa96dsga5nk8w/geoportail/wmts", - "protocol": "wmts", "id": "Cadastre", "title": "Parcelles cadastrales", "visible": false, "opacity": 1, "transparent": true, - "updateStrategy": { - "type": 0, - "options": {} - }, - "networkOptions": { - "crossOrigin": "omit" - }, - "format": "image/png", - "options": { + "source": { + "protocol": "wmts", + "url": "https://wxs.ign.fr/an7nvfzojv5wa96dsga5nk8w/geoportail/wmts", + "networkOptions": { + "crossOrigin": "omit" + }, + "version": "1.0.0", + "name": "CADASTRALPARCELS.PARCELS", + "style": "bdparcellaire", + "zoom": { + "min": 5, + "max": 20 + }, + "format": "image/png", "tileMatrixSet": "PM", "tileMatrixSetLimits": { "0": { @@ -150,13 +153,6 @@ "minTileCol": "0", "maxTileCol": "2097152" } - }, - "name": "CADASTRALPARCELS.PARCELS", - "style": "bdparcellaire", - "zoom": { - "min": 5, - "max": 20 } - }, - "version": "1.0.0" + } } diff --git a/examples/layers/JSONLayers/DARK.json b/examples/layers/JSONLayers/DARK.json index 7f81a8b0e1..57fa54d47e 100644 --- a/examples/layers/JSONLayers/DARK.json +++ b/examples/layers/JSONLayers/DARK.json @@ -1,11 +1,11 @@ { "type": "color", - "protocol": "xyz", - "networkOptions": { "crossOrigin" : "anonymous" }, "id": "DARK", - "url": "http://a.basemaps.cartocdn.com/dark_all/${z}/${x}/${y}.png", - "format": "image/png", - "options": { + "source": { + "protocol": "xyz", + "networkOptions": { "crossOrigin" : "anonymous" }, + "format": "image/png", + "url": "http://a.basemaps.cartocdn.com/dark_all/${z}/${x}/${y}.png", "attribution": { "name":"CARTO", "url": "https://carto.com/" diff --git a/examples/layers/JSONLayers/Denomination.json b/examples/layers/JSONLayers/Denomination.json index 7a99dcfcca..c6624df611 100644 --- a/examples/layers/JSONLayers/Denomination.json +++ b/examples/layers/JSONLayers/Denomination.json @@ -1,24 +1,20 @@ { "type": "color", - "url": "https://wxs.ign.fr/an7nvfzojv5wa96dsga5nk8w/geoportail/wmts", - "protocol": "wmts", "id": "Denomination", "title": "Geographical denomination in France", "visible": false, "opacity": 1, "transparent": true, - "format": "image/png", - "updateStrategy": { - "type": 0, - "options": {} - }, - "networkOptions": { - "crossOrigin": "omit" - }, - "options": { + "source": { + "protocol": "wmts", + "url": "https://wxs.ign.fr/an7nvfzojv5wa96dsga5nk8w/geoportail/wmts", "tileMatrixSet": "PM", + "format": "image/png", "name": "GEOGRAPHICALNAMES.NAMES", "style": "normal", + "networkOptions": { + "crossOrigin": "omit" + }, "zoom": { "min": 6, "max": 20 diff --git a/examples/layers/JSONLayers/IGN_MNT.json b/examples/layers/JSONLayers/IGN_MNT.json index e7de637532..3f01086bf0 100644 --- a/examples/layers/JSONLayers/IGN_MNT.json +++ b/examples/layers/JSONLayers/IGN_MNT.json @@ -1,8 +1,6 @@ { "type": "elevation", - "protocol": "wmts", "id": "IGN_MNT", - "url": "http://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/wmts", "noDataValue" : -99999, "updateStrategy": { "type": 1, @@ -10,8 +8,10 @@ "groups": [3, 7, 11] } }, - "format": "image/x-bil;bits=32", - "options": { + "source": { + "protocol": "wmts", + "url": "http://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/wmts", + "format": "image/x-bil;bits=32", "attribution" : { "name":"IGN", "url":"http://www.ign.fr/" diff --git a/examples/layers/JSONLayers/IGN_MNT_HIGHRES.json b/examples/layers/JSONLayers/IGN_MNT_HIGHRES.json index 45c3c9f2f6..79db1470bf 100644 --- a/examples/layers/JSONLayers/IGN_MNT_HIGHRES.json +++ b/examples/layers/JSONLayers/IGN_MNT_HIGHRES.json @@ -1,8 +1,6 @@ { "type": "elevation", - "protocol": "wmts", "id": "IGN_MNT_HIGHRES", - "url": "http://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/wmts", "noDataValue" : -99999, "updateStrategy": { "type": 1, @@ -10,8 +8,10 @@ "groups": [11, 14] } }, - "format": "image/x-bil;bits=32", - "options": { + "source": { + "protocol": "wmts", + "url": "http://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/wmts", + "format": "image/x-bil;bits=32", "attribution" : { "name":"IGN", "url":"http://www.ign.fr/" diff --git a/examples/layers/JSONLayers/OPENSM.json b/examples/layers/JSONLayers/OPENSM.json index 1aa7757390..7c7e37f5cc 100644 --- a/examples/layers/JSONLayers/OPENSM.json +++ b/examples/layers/JSONLayers/OPENSM.json @@ -1,11 +1,10 @@ { "type": "color", - "protocol": "xyz", - "networkOptions": { "crossOrigin" : "anonymous" }, "id": "OPENSM", - "url": "http://osm.oslandia.io/styles/klokantech-basic/${z}/${x}/${y}.png", - "format": "image/png", - "options": { + "source": { + "protocol": "xyz", + "format": "image/png", + "url": "http://osm.oslandia.io/styles/klokantech-basic/${z}/${x}/${y}.png", "attribution": { "name":"OpenStreetMap", "url": "http://www.openstreetmap.org/" diff --git a/examples/layers/JSONLayers/Ortho.json b/examples/layers/JSONLayers/Ortho.json index 0cc784dbc5..73fa4f7d1f 100644 --- a/examples/layers/JSONLayers/Ortho.json +++ b/examples/layers/JSONLayers/Ortho.json @@ -1,17 +1,13 @@ { "type": "color", - "protocol": "wmts", - "id": "Ortho", - "url": "http://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/wmts", - "networkOptions": { - "crossOrigin": "anonymous" - }, - "updateStrategy": { - "type": 0, - "options": {} - }, - "format": "image/jpeg", - "options": { + "id": "Ortho", + "source": { + "protocol": "wmts", + "url": "http://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/wmts", + "networkOptions": { + "crossOrigin": "anonymous" + }, + "format": "image/jpeg", "attribution" : { "name":"IGN", "url":"http://www.ign.fr/" diff --git a/examples/layers/JSONLayers/OrthosCRS.json b/examples/layers/JSONLayers/OrthosCRS.json index 4178a0af82..f26e8e2990 100644 --- a/examples/layers/JSONLayers/OrthosCRS.json +++ b/examples/layers/JSONLayers/OrthosCRS.json @@ -1,18 +1,11 @@ { "type": "color", - "protocol": "wmts", "id": "OrthoCRS", - "url": "http://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/wmts", - "networkOptions": { - "crossOrigin": "anonymous" - }, "fx" : 1.0, - "updateStrategy": { - "type": 0, - "options": {} - }, - "format": "image/jpeg", - "options": { + "source": { + "protocol": "wmts", + "url": "http://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/wmts", + "format": "image/jpeg", "name": "ORTHOIMAGERY.ORTHOPHOTOS.CRS84", "attribution" : { "name":"IGN", diff --git a/examples/layers/JSONLayers/Railways.json b/examples/layers/JSONLayers/Railways.json index a38b968033..aa105eb2ed 100644 --- a/examples/layers/JSONLayers/Railways.json +++ b/examples/layers/JSONLayers/Railways.json @@ -1,21 +1,17 @@ { "type": "color", - "url": "https://wxs.ign.fr/an7nvfzojv5wa96dsga5nk8w/geoportail/wmts", - "protocol": "wmts", "id": "Railways", "title": "Railways in France", "visible": false, "opacity": 1, "transparent": true, - "format": "image/png", - "updateStrategy": { - "type": 0, - "options": {} - }, - "networkOptions": { - "crossOrigin": "omit" - }, - "options": { + "source": { + "protocol": "wmts", + "url": "https://wxs.ign.fr/an7nvfzojv5wa96dsga5nk8w/geoportail/wmts", + "format": "image/png", + "networkOptions": { + "crossOrigin": "omit" + }, "tileMatrixSet": "PM", "name": "TRANSPORTNETWORKS.RAILWAYS", "style": "normal", diff --git a/examples/layers/JSONLayers/Region.json b/examples/layers/JSONLayers/Region.json index e058febeca..c0684d5d18 100644 --- a/examples/layers/JSONLayers/Region.json +++ b/examples/layers/JSONLayers/Region.json @@ -1,32 +1,30 @@ { "type": "color", - "url" : "https://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/v/wms", - "networkOptions": { - "crossOrigin": "anonymous" - }, - "protocol" : "wms", - "version" : "1.3.0", "id" : "Region", - "name" : "REGION.2016", - "style" : "", - "projection" : "EPSG:3857", - "extent": { - "west": "-6880639.13557728", - "east": "6215707.87974825", - "south": "-2438399.00148845", - "north": "6637050.03850605" - }, "transparent" : true, - "featureInfoMimeType" : "", - "dateTime" : "", - "heightMapWidth" : 256, - "waterMask" : false, - "updateStrategy": { - "type": 0, - "options": {} - }, - "format" : "image/png", - "options": { + "source": { + "url" : "https://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/v/wms", + "protocol" : "wms", + "version" : "1.3.0", + "name" : "REGION.2016", + "style" : "", + "projection" : "EPSG:3857", + "extent": { + "west": "-6880639.13557728", + "east": "6215707.87974825", + "south": "-2438399.00148845", + "north": "7637050.03850605" + }, + "transparent" : true, + "featureInfoMimeType" : "", + "dateTime" : "", + "heightMapWidth" : 256, + "waterMask" : false, + "updateStrategy": { + "type": 0, + "options": {} + }, + "format" : "image/png", "attribution" : { "name":"IGN", "url":"http://www.ign.fr/" diff --git a/examples/layers/JSONLayers/ScanEX.json b/examples/layers/JSONLayers/ScanEX.json index ae98c54c98..9d90f21b76 100644 --- a/examples/layers/JSONLayers/ScanEX.json +++ b/examples/layers/JSONLayers/ScanEX.json @@ -1,138 +1,131 @@ { "type": "color", - "protocol": "wmts", "id": "ScanEX", - "url": "http://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/wmts", - "networkOptions": { - "crossOrigin": "anonymous" - }, "fx" : 2.5, - "updateStrategy": { - "type": 0, - "options": {} - }, - "format": "image/jpeg", - "options": { + "source": { + "protocol": "wmts", + "url": "http://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/wmts", + "format": "image/jpeg", + "name": "GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN-EXPRESS.STANDARD", "attribution" : { "name":"IGN", "url":"http://www.ign.fr/" }, - "name": "GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN-EXPRESS.STANDARD", "tileMatrixSet": "PM", "tileMatrixSetLimits": { "0": { - "MinTileRow": 0, - "MaxTileRow": 0, - "MinTileCol": 0, - "MaxTileCol": 1 + "minTileRow": 0, + "maxTileRow": 0, + "minTileCol": 0, + "maxTileCol": 1 }, "1": { - "MinTileRow": 0, - "MaxTileRow": 1, - "MinTileCol": 0, - "MaxTileCol": 2 + "minTileRow": 0, + "maxTileRow": 1, + "minTileCol": 0, + "maxTileCol": 2 }, "2": { - "MinTileRow": 0, - "MaxTileRow": 3, - "MinTileCol": 0, - "MaxTileCol": 4 + "minTileRow": 0, + "maxTileRow": 3, + "minTileCol": 0, + "maxTileCol": 4 }, "3": { - "MinTileRow": 0, - "MaxTileRow": 7, - "MinTileCol": 0, - "MaxTileCol": 8 + "minTileRow": 0, + "maxTileRow": 7, + "minTileCol": 0, + "maxTileCol": 8 }, "4": { - "MinTileRow": 0, - "MaxTileRow": 15, - "MinTileCol": 0, - "MaxTileCol": 16 + "minTileRow": 0, + "maxTileRow": 15, + "minTileCol": 0, + "maxTileCol": 16 }, "5": { - "MinTileRow": 0, - "MaxTileRow": 30, - "MinTileCol": 0, - "MaxTileCol": 32 + "minTileRow": 0, + "maxTileRow": 30, + "minTileCol": 0, + "maxTileCol": 32 }, "6": { - "MinTileRow": 0, - "MaxTileRow": 62, - "MinTileCol": 0, - "MaxTileCol": 64 + "minTileRow": 0, + "maxTileRow": 62, + "minTileCol": 0, + "maxTileCol": 64 }, "7": { - "MinTileRow": 0, - "MaxTileRow": 124, - "MinTileCol": 0, - "MaxTileCol": 128 + "minTileRow": 0, + "maxTileRow": 124, + "minTileCol": 0, + "maxTileCol": 128 }, "8": { - "MinTileRow": 85, - "MaxTileRow": 143, - "MinTileCol": 81, - "MaxTileCol": 167 + "minTileRow": 85, + "maxTileRow": 143, + "minTileCol": 81, + "maxTileCol": 167 }, "9": { - "MinTileRow": 170, - "MaxTileRow": 287, - "MinTileCol": 163, - "MaxTileCol": 335 + "minTileRow": 170, + "maxTileRow": 287, + "minTileCol": 163, + "maxTileCol": 335 }, "10": { - "MinTileRow": 340, - "MaxTileRow": 574, - "MinTileCol": 331, - "MaxTileCol": 671 + "minTileRow": 340, + "maxTileRow": 574, + "minTileCol": 331, + "maxTileCol": 671 }, "11": { - "MinTileRow": 681, - "MaxTileRow": 114, - "MinTileCol": 663, - "MaxTileCol": 134 + "minTileRow": 681, + "maxTileRow": 114, + "minTileCol": 663, + "maxTileCol": 134 }, "12": { - "MinTileRow": 1363, - "MaxTileRow": 2298, - "MinTileCol": 1326, - "MaxTileCol": 2684 + "minTileRow": 1363, + "maxTileRow": 2298, + "minTileCol": 1326, + "maxTileCol": 2684 }, "13": { - "MinTileRow": 2726, - "MaxTileRow": 4596, - "MinTileCol": 2653, - "MaxTileCol": 5368 + "minTileRow": 2726, + "maxTileRow": 4596, + "minTileCol": 2653, + "maxTileCol": 5368 }, "14": { - "MinTileRow": 5452, - "MaxTileRow": 9204, - "MinTileCol": 5311, - "MaxTileCol": 1074 + "minTileRow": 5452, + "maxTileRow": 9204, + "minTileCol": 5311, + "maxTileCol": 1074 }, "15": { - "MinTileRow": 10944, - "MaxTileRow": 18381, - "MinTileCol": 10632, - "MaxTileCol": 21467 + "minTileRow": 10944, + "maxTileRow": 18381, + "minTileCol": 10632, + "maxTileCol": 21467 }, "16": { - "MinTileRow": 21889, - "MaxTileRow": 36763, - "MinTileCol": 21264, - "MaxTileCol": 42934 + "minTileRow": 21889, + "maxTileRow": 36763, + "minTileCol": 21264, + "maxTileCol": 42934 }, "17": { - "MinTileRow": 43778, - "MaxTileRow": 73526, - "MinTileCol": 42528, - "MaxTileCol": 85869 + "minTileRow": 43778, + "maxTileRow": 73526, + "minTileCol": 42528, + "maxTileCol": 85869 }, "18": { - "MinTileRow": 87557, - "MaxTileRow": 14705, - "MinTileCol": 85058, - "MaxTileCol": 17173 + "minTileRow": 87557, + "maxTileRow": 14705, + "minTileCol": 85058, + "maxTileCol": 17173 } } } diff --git a/examples/layers/JSONLayers/Transport.json b/examples/layers/JSONLayers/Transport.json index 36a38465de..9898efee61 100644 --- a/examples/layers/JSONLayers/Transport.json +++ b/examples/layers/JSONLayers/Transport.json @@ -1,21 +1,17 @@ { "type": "color", - "url": "https://wxs.ign.fr/an7nvfzojv5wa96dsga5nk8w/geoportail/wmts", - "protocol": "wmts", "id": "Transport", "title": "Transport in France", "visible": false, "opacity": 1, "transparent": true, - "format": "image/png", - "updateStrategy": { - "type": 0, - "options": {} - }, - "networkOptions": { - "crossOrigin": "omit" - }, - "options": { + "source": { + "protocol": "wmts", + "url": "https://wxs.ign.fr/an7nvfzojv5wa96dsga5nk8w/geoportail/wmts", + "format": "image/png", + "networkOptions": { + "crossOrigin": "omit" + }, "tileMatrixSet": "PM", "name": "TRANSPORTNETWORKS.ROADS", "style": "normal", diff --git a/examples/layers/JSONLayers/WORLD_DTM.json b/examples/layers/JSONLayers/WORLD_DTM.json index 18b8c8d290..97d9f77ae6 100644 --- a/examples/layers/JSONLayers/WORLD_DTM.json +++ b/examples/layers/JSONLayers/WORLD_DTM.json @@ -1,9 +1,7 @@ { "type": "elevation", - "protocol": "wmts", "id": "MNT_WORLD_SRTM3", - "url": "http://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/wmts", "noDataValue": -99999, "updateStrategy": { "type": 1, @@ -11,8 +9,10 @@ "groups": [3, 7, 9] } }, - "format": "image/x-bil;bits=32", - "options": { + "source": { + "format": "image/x-bil;bits=32", + "protocol": "wmts", + "url": "http://wxs.ign.fr/va5orxd0pgzvq3jxutqfuy0b/geoportail/wmts", "name": "ELEVATION.ELEVATIONGRIDCOVERAGE.SRTM3", "tileMatrixSet": "WGS84G", "tileMatrixSetLimits": { diff --git a/examples/orthographic.html b/examples/orthographic.html index 534a4a9715..9fd4bdac8a 100644 --- a/examples/orthographic.html +++ b/examples/orthographic.html @@ -25,7 +25,9 @@ var viewerDiv = document.getElementById('viewerDiv'); // Instanciate PlanarView - var view = new itowns.PlanarView(viewerDiv, extent, { maxSubdivisionLevel: 10 }); + // By default itowns' tiles geometry have a "skirt" (ie they have a height), + // but in case of orthographic we don't need this feature, so disable it + var view = new itowns.PlanarView(viewerDiv, extent, { disableSkirt: true, maxSubdivisionLevel: 10 }); // eslint-disable-next-line no-new new itowns.PlanarControls(view, { @@ -44,29 +46,25 @@ itowns.CameraUtils.transformCameraToLookAtTarget(view, view.camera.camera3D, { tilt: 90 }); setupLoadingScreen(viewerDiv, view); - // By default itowns' tiles geometry have a "skirt" (ie they have a height), - // but in case of orthographic we don't need this feature, so disable it - view.tileLayer.disableSkirt = true; - // Add a TMS imagery layer view.addLayer({ type: 'color', - protocol: 'xyz', id: 'OPENSM', - // eslint-disable-next-line no-template-curly-in-string - url: 'http://c.tile.stamen.com/watercolor/${z}/${x}/${y}.jpg', - networkOptions: { crossOrigin: 'anonymous' }, - extent: [extent.west(), extent.east(), extent.south(), extent.north()], - projection: 'EPSG:3857', - options: { + updateStrategy: { + type: itowns.STRATEGY_DICHOTOMY, + }, + source: { + protocol: 'xyz', + // eslint-disable-next-line no-template-curly-in-string + url: 'http://c.tile.stamen.com/watercolor/${z}/${x}/${y}.jpg', + networkOptions: { crossOrigin: 'anonymous' }, + extent: [extent.west(), extent.east(), extent.south(), extent.north()], + projection: 'EPSG:3857', attribution: { name: 'OpenStreetMap', url: 'http://www.openstreetmap.org/', }, }, - updateStrategy: { - type: itowns.STRATEGY_DICHOTOMY, - }, }); // Request redraw diff --git a/examples/panorama.html b/examples/panorama.html index 2f1939b93d..bb1e979e04 100644 --- a/examples/panorama.html +++ b/examples/panorama.html @@ -50,12 +50,13 @@ var _id = url.pathname; view.addLayer({ visible: index == activeIndex, - url: url.href, - networkOptions: { crossOrigin: 'anonymous' }, type: 'color', - protocol: 'static', id: _id, - projection: 'EPSG:4326', + source: { + url: url.href, + protocol: 'static', + projection: 'EPSG:4326', + }, updateStrategy: { type: itowns.STRATEGY_DICHOTOMY, } @@ -71,7 +72,7 @@ var gui = new dat.GUI(); var ddd = new debug.Debug(view, gui); debug.createTileDebugUI(gui, view, view.baseLayer, ddd); - gui.add(view.baseLayer, 'quality').min(0.1).max(1.0).onChange( + gui.add(view.baseLayer.options, 'quality').min(0.1).max(1.0).onChange( function () { view.notifyChange(); }); // Add controls diff --git a/examples/planar.html b/examples/planar.html index 2518b61918..d654ad9b45 100644 --- a/examples/planar.html +++ b/examples/planar.html @@ -37,6 +37,7 @@ var p; var menuGlobe; var d; + var config; // Define projection that we will use (taken from https://epsg.io/3946, Proj4js section) itowns.proj4.defs('EPSG:3946', @@ -51,46 +52,53 @@ // `viewerDiv` will contain iTowns' rendering area (``) viewerDiv = document.getElementById('viewerDiv'); + // Since the elevation layer use color textures, specify min/max z + config = { + materialOptions: { + useColorTextureElevation: true, + colorTextureElevationMinZ: 37, + colorTextureElevationMaxZ: 240, + }, + disableSkirt: true, + }; + // Instanciate PlanarView* - view = new itowns.PlanarView(viewerDiv, extent); + view = new itowns.PlanarView(viewerDiv, extent, config); setupLoadingScreen(viewerDiv, view); - view.tileLayer.disableSkirt = true; // Add an WMS imagery layer (see WMSProvider* for valid options) view.addLayer({ - url: 'https://download.data.grandlyon.com/wms/grandlyon', - networkOptions: { crossOrigin: 'anonymous' }, type: 'color', - protocol: 'wms', - version: '1.3.0', id: 'wms_imagery', - name: 'Ortho2009_vue_ensemble_16cm_CC46', - projection: 'EPSG:3946', - format: 'image/jpeg', updateStrategy: { type: itowns.STRATEGY_DICHOTOMY, options: {}, }, + source: { + extent: extent, + name: 'Ortho2009_vue_ensemble_16cm_CC46', + protocol: 'wms', + url: 'https://download.data.grandlyon.com/wms/grandlyon', + version: '1.3.0', + projection: 'EPSG:3946', + format: 'image/jpeg', + }, }); // Add an WMS elevation layer (see WMSProvider* for valid options) view.addLayer({ - url: 'https://download.data.grandlyon.com/wms/grandlyon', - type: 'elevation', - protocol: 'wms', - networkOptions: { crossOrigin: 'anonymous' }, id: 'wms_elevation', - name: 'MNT2012_Altitude_10m_CC46', - projection: 'EPSG:3946', - heightMapWidth: 256, - format: 'image/jpeg', + type: 'elevation', + source: { + extent: extent, + url: 'https://download.data.grandlyon.com/wms/grandlyon', + protocol: 'wms', + name: 'MNT2012_Altitude_10m_CC46', + projection: 'EPSG:3946', + heightMapWidth: 256, + format: 'image/jpeg', + }, }); - // Since the elevation layer use color textures, specify min/max z - view.tileLayer.materialOptions = { - useColorTextureElevation: true, - colorTextureElevationMinZ: 37, - colorTextureElevationMaxZ: 240, - }; p = { coord: extent.center(), heading: -49.6, range: 6200, tilt: 17 }; itowns.CameraUtils.transformCameraToLookAtTarget(view, view.camera.camera3D, p); diff --git a/examples/planar_vector.html b/examples/planar_vector.html index d9503c8ccd..cc120e51e2 100644 --- a/examples/planar_vector.html +++ b/examples/planar_vector.html @@ -31,6 +31,7 @@ var viewerDiv; var view; var p; + var config; // Define projection that we will use (taken from https://epsg.io/3946, Proj4js section) itowns.proj4.defs('EPSG:3946', @@ -45,70 +46,91 @@ // `viewerDiv` will contain iTowns' rendering area (``) viewerDiv = document.getElementById('viewerDiv'); + // Since the elevation layer use color textures, specify min/max z + config = { + materialOptions: { + useColorTextureElevation: true, + colorTextureElevationMinZ: 37, + colorTextureElevationMaxZ: 240, + }, + disableSkirt: true, + }; + // Instanciate PlanarView* - view = new itowns.PlanarView(viewerDiv, extent); + view = new itowns.PlanarView(viewerDiv, extent, config); setupLoadingScreen(viewerDiv, view); - view.tileLayer.disableSkirt = true; // Add an WMS imagery layer (see WMSProvider* for valid options) view.addLayer({ - url: 'https://download.data.grandlyon.com/wms/grandlyon', - networkOptions: { crossOrigin: 'anonymous' }, - type: 'color', - protocol: 'wms', - version: '1.3.0', id: 'wms_imagery', - name: 'Ortho2009_vue_ensemble_16cm_CC46', - projection: 'EPSG:3946', + type: 'color', transparent: true, - format: 'image/jpeg', + source: { + extent: extent, + url: 'https://download.data.grandlyon.com/wms/grandlyon', + networkOptions: { crossOrigin: 'anonymous' }, + name: 'Ortho2009_vue_ensemble_16cm_CC46', + protocol: 'wms', + version: '1.3.0', + projection: 'EPSG:3946', + format: 'image/jpeg', + }, }); // Add an WMS elevation layer (see WMSProvider* for valid options) view.addLayer({ - url: 'https://download.data.grandlyon.com/wms/grandlyon', - type: 'elevation', - protocol: 'wms', - networkOptions: { crossOrigin: 'anonymous' }, - version: '1.3.0', id: 'wms_elevation', - name: 'MNT2012_Altitude_10m_CC46', - projection: view.referenceCrs, - heightMapWidth: 256, - format: 'image/jpeg', + type: 'elevation', + source: { + extent: extent, + url: 'https://download.data.grandlyon.com/wms/grandlyon', + protocol: 'wms', + networkOptions: { crossOrigin: 'anonymous' }, + name: 'MNT2012_Altitude_10m_CC46', + projection: 'EPSG:3946', + heightMapWidth: 256, + format: 'image/jpeg', + }, }); view.addLayer({ type: 'color', - url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/lyon.kml', - protocol: 'rasterizer', id: 'Kml', - extent: extent, transparent: true, - options: { zoom: { min: 0, max: 6 } }, + source: { + url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/lyon.kml', + protocol: 'file', + projection: 'EPSG:4326', + extent: extent, + zoom: { min: 0, max: 6 }, + }, }); view.addLayer({ type: 'color', - url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/lyon.gpx', - protocol: 'rasterizer', - options: { zoom: { min: 0, max: 6 } }, id: 'gpx', transparent: true, style: { stroke: 'blue', }, + source: { + url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/lyon.gpx', + protocol: 'file', + zoom: { min: 0, max: 6 }, + }, }); view.addLayer({ type: 'color', - url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/lyon.geojson', - protocol: 'rasterizer', - projection: 'EPSG:3946', id: 'geo', - extent: extent, transparent: true, - options: { zoom: { min: 0, max: 6 } }, + source: { + url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/lyon.geojson', + protocol: 'file', + projection: 'EPSG:3946', + extent: extent, + zoom: { min: 0, max: 6 }, + }, style: { fill: 'orange', fillOpacity: 0.2, @@ -116,13 +138,6 @@ }, }); - // Since the elevation layer use color textures, specify min/max z - view.tileLayer.materialOptions = { - useColorTextureElevation: true, - colorTextureElevationMinZ: 37, - colorTextureElevationMaxZ: 240, - }; - p = { coord: extent.center(), heading: -49.6, range: 6200, tilt: 17 }; itowns.CameraUtils.transformCameraToLookAtTarget(view, view.camera.camera3D, p); diff --git a/examples/planar_vector_tiles.html b/examples/planar_vector_tiles.html index 0bacaf54ef..1a392e1622 100644 --- a/examples/planar_vector_tiles.html +++ b/examples/planar_vector_tiles.html @@ -60,16 +60,19 @@ } }); + function isValidData(data, extentDestination) { + return extentDestination.zoom - data.extent.zoom < 4; + } + view.addLayer({ type: 'color', - protocol: 'xyz', id: 'MVT', - // eslint-disable-next-line no-template-curly-in-string - url: 'https://osm.oslandia.io/data/v3/${z}/${x}/${y}.pbf', - extent, - projection: 'EPSG:3857', - format: 'application/x-protobuf;type=mapbox-vector', - options: { + isValidData: isValidData, + source: { + protocol: 'xyz', + url: 'https://osm.oslandia.io/data/v3/${z}/${x}/${y}.pbf', + format: 'application/x-protobuf;type=mapbox-vector', + projection: 'EPSG:4326', attribution: { name: 'OpenStreetMap', url: 'http://www.openstreetmap.org/', @@ -78,13 +81,13 @@ min: 2, max: 15, }, + // eslint-disable-next-line no-template-curly-in-string + extent: extent.as('EPSG:4326'), }, - updateStrategy: { - type: itowns.STRATEGY_DICHOTOMY, - }, - style: mapboxStyle, filter: mapboxFilter(supportedLayers), + style: mapboxStyle, backgroundLayer, + projection: 'EPSG:3857', }); }); diff --git a/examples/syncCameras.html b/examples/syncCameras.html index 6d3929c8c9..aac11a5052 100644 --- a/examples/syncCameras.html +++ b/examples/syncCameras.html @@ -132,19 +132,21 @@ }); planarView.addLayer({ - url: 'https://download.data.grandlyon.com/wms/grandlyon', - networkOptions: { crossOrigin: 'anonymous' }, type: 'color', - protocol: 'wms', - version: '1.3.0', id: 'wms_imagery', - name: 'Ortho2009_vue_ensemble_16cm_CC46', - projection: 'EPSG:3946', - format: 'image/jpeg', updateStrategy: { type: itowns.STRATEGY_DICHOTOMY, options: {}, }, + source: { + extent: extent, + name: 'Ortho2009_vue_ensemble_16cm_CC46', + protocol: 'wms', + url: 'https://download.data.grandlyon.com/wms/grandlyon', + version: '1.3.0', + projection: 'EPSG:3946', + format: 'image/jpeg', + }, }); var d = new debug.Debug(globeView, menuGlobe.gui); debug.createTileDebugUI(menuGlobe.gui, globeView, globeView.wgs84TileLayer, d); diff --git a/examples/wfs.html b/examples/wfs.html index fb324e110c..10a02245ad 100644 --- a/examples/wfs.html +++ b/examples/wfs.html @@ -52,23 +52,24 @@ viewerDiv = document.getElementById('viewerDiv'); // Instanciate PlanarView* - view = new itowns.PlanarView(viewerDiv, extent); + view = new itowns.PlanarView(viewerDiv, extent, { disableSkirt: true }); setupLoadingScreen(viewerDiv, view); - view.tileLayer.disableSkirt = true; // Add an WMS imagery layer (see WMSProvider* for valid options) view.addLayer({ - url: 'https://download.data.grandlyon.com/wms/grandlyon', - networkOptions: { crossOrigin: 'anonymous' }, type: 'color', - protocol: 'wms', - version: '1.3.0', id: 'wms_imagery', - name: 'Ortho2009_vue_ensemble_16cm_CC46', - projection: 'EPSG:3946', transparent: false, - extent: extent, - format: 'image/jpeg', + source: { + url: 'https://download.data.grandlyon.com/wms/grandlyon', + networkOptions: { crossOrigin: 'anonymous' }, + protocol: 'wms', + version: '1.3.0', + name: 'Ortho2009_vue_ensemble_16cm_CC46', + projection: 'EPSG:3946', + extent: extent, + format: 'image/jpeg', + }, }); p = { coord: new itowns.Coordinates('EPSG:3946', 1840839, 5172718, 0), heading: -45, range: 1800, tilt: 30 }; @@ -100,20 +101,22 @@ convert: itowns.Feature2Mesh.convert({ color: colorLine }), onMeshCreated: setMaterialLineWidth, - url: 'https://download.data.grandlyon.com/wfs/rdata?', - protocol: 'wfs', - version: '2.0.0', - id: 'tcl_bus', - typeName: 'tcl_sytral.tcllignebus', - level: 2, - projection: 'EPSG:3946', - extent: { - west: 1822174.60, - east: 1868247.07, - south: 5138876.75, - north: 5205890.19, + source: { + url: 'https://download.data.grandlyon.com/wfs/rdata?', + protocol: 'wfs', + version: '2.0.0', + id: 'tcl_bus', + typeName: 'tcl_sytral.tcllignebus', + projection: 'EPSG:3946', + extent: { + west: 1822174.60, + east: 1868247.07, + south: 5138876.75, + north: 5205890.19, + }, + zoom: { min: 2, max: 2 }, + format: 'geojson', }, - format: 'geojson', }); function colorBuildings(properties) { @@ -147,6 +150,7 @@ view.addFrameRequester(itowns.MAIN_LOOP_EVENTS.BEFORE_RENDER, scaler); view.addLayer({ + id: 'wfsBuilding', type: 'geometry', update: itowns.FeatureProcessing.update, convert: itowns.Feature2Mesh.convert({ @@ -156,22 +160,23 @@ mesh.scale.z = 0.01; meshes.push(mesh); }, - url: 'http://wxs.ign.fr/72hpsel8j8nhb5qgdh07gcyp/geoportail/wfs?', - networkOptions: { crossOrigin: 'anonymous' }, - protocol: 'wfs', - version: '2.0.0', - id: 'wfsBuilding', - typeName: 'BDTOPO_BDD_WLD_WGS84G:bati_remarquable,BDTOPO_BDD_WLD_WGS84G:bati_indifferencie,BDTOPO_BDD_WLD_WGS84G:bati_industriel', - level: 5, - projection: 'EPSG:4326', - extent: { - west: 4.568, - east: 5.18, - south: 45.437, - north: 46.03, + projection: 'EPSG:3946', + source: { + url: 'http://wxs.ign.fr/72hpsel8j8nhb5qgdh07gcyp/geoportail/wfs?', + protocol: 'wfs', + version: '2.0.0', + typeName: 'BDTOPO_BDD_WLD_WGS84G:bati_remarquable,BDTOPO_BDD_WLD_WGS84G:bati_indifferencie,BDTOPO_BDD_WLD_WGS84G:bati_industriel', + projection: 'EPSG:4326', + ipr: 'IGN', + format: 'application/json', + zoom: { min: 5, max: 5 }, + extent: { + west: 4.568, + east: 5.18, + south: 45.437, + north: 46.03, + }, }, - ipr: 'IGN', - format: 'application/json', }); function configPointMaterial(result) { @@ -193,22 +198,29 @@ } view.addLayer({ + id: 'wfsPoint', type: 'geometry', update: itowns.FeatureProcessing.update, convert: itowns.Feature2Mesh.convert({ altitude: 0, color: colorPoint }), onMeshCreated: configPointMaterial, - url: 'http://wxs.ign.fr/72hpsel8j8nhb5qgdh07gcyp/geoportail/wfs?', - networkOptions: { crossOrigin: 'anonymous' }, - protocol: 'wfs', - version: '2.0.0', - id: 'wfsPoint', - typeName: 'BDPR_BDD_FXX_LAMB93_20170911:pr', - level: 2, - projection: 'EPSG:2154', - ipr: 'IGN', - format: 'application/json', + source: { + url: 'http://wxs.ign.fr/72hpsel8j8nhb5qgdh07gcyp/geoportail/wfs?', + protocol: 'wfs', + version: '2.0.0', + id: 'WFS Route points', + typeName: 'BDPR_BDD_FXX_LAMB93_20170911:pr', + zoom: { min: 2, max: 2 }, + projection: 'EPSG:2154', + ipr: 'IGN', + format: 'application/json', + extent: new itowns.Extent('EPSG:4326', + -5.160007066832147, + 10.671941031325312, + 41.338373237911036, + 51.07519774631159).as('EPSG:2154'), + }, }, view.tileLayer); /* global, document, window, view */ diff --git a/jsdoc-config.json b/jsdoc-config.json index 5080213d90..e0c1078df2 100644 --- a/jsdoc-config.json +++ b/jsdoc-config.json @@ -12,6 +12,7 @@ "src/Core/MainLoop.js", "src/Core/View.js", "src/Core/Geographic/Coordinates.js", + "src/Core/Geographic/Extent.js", "src/Core/Prefab/GlobeView.js", "src/Core/Prefab/Globe/GlobeLayer.js", "src/Core/Prefab/Panorama/PanoramaLayer.js", @@ -25,6 +26,14 @@ "src/Layer/GeometryLayer.js", "src/Layer/TiledGeometryLayer.js", + "src/Source/Source.js", + "src/Source/WMTSSource.js", + "src/Source/WMSSource.js", + "src/Source/WFSSource.js", + "src/Source/TMSSource.js", + "src/Source/StaticSource.js", + "src/Source/FileSource.js", + "src/Parser/GeoJsonParser.js", "src/Parser/GpxParser.js", "src/Parser/VectorTileParser.js", diff --git a/src/Core/Geographic/Extent.js b/src/Core/Geographic/Extent.js index 114d23cdbe..03ed3ffde0 100644 --- a/src/Core/Geographic/Extent.js +++ b/src/Core/Geographic/Extent.js @@ -14,15 +14,15 @@ const CARDINAL = { NORTH: 3, }; -function _isTiledCRS(crs) { - return crs.indexOf('WMTS:') == 0 || - crs == 'TMS'; -} - +/** + * @class Extent + * @param {string} crs projection crs (ex: 'EPSG:4326') + * @param {(Array|object|Number)} values west, east, south and north values + */ function Extent(crs, ...values) { this._crs = crs; - if (_isTiledCRS(crs)) { + if (this.isTiledCrs()) { if (values.length == 3) { this.zoom = values[0]; this.row = values[1]; @@ -60,7 +60,7 @@ function Extent(crs, ...values) { } Extent.prototype.clone = function clone() { - if (_isTiledCRS(this._crs)) { + if (this.isTiledCrs()) { return new Extent(this._crs, this.zoom, this.row, this.col); } else { const result = new Extent(this._crs, ...this._values); @@ -68,11 +68,15 @@ Extent.prototype.clone = function clone() { } }; +Extent.prototype.isTiledCrs = function fnIsTiledCrs() { + return this._crs.indexOf('WMTS:') == 0 || this._crs == 'TMS'; +}; + Extent.prototype.as = function as(crs) { assertCrsIsValid(crs); - if (_isTiledCRS(this._crs)) { - if (this._crs == 'WMTS:PM') { + if (this.isTiledCrs()) { + if (this._crs == 'WMTS:PM' || this._crs == 'TMS') { // Convert this to the requested crs by using 4326 as an intermediate state. const nbCol = Math.pow(2, this.zoom); const size = 360 / nbCol; @@ -145,7 +149,7 @@ Extent.prototype.offsetToParent = function offsetToParent(other, target = new TH if (this.crs() != other.crs()) { throw new Error('unsupported mix'); } - if (_isTiledCRS(this.crs())) { + if (this.isTiledCrs()) { const diffLevel = this.zoom - other.zoom; const diff = Math.pow(2, diffLevel); const invDiff = 1 / diff; @@ -201,7 +205,7 @@ Extent.prototype.crs = function crs() { }; Extent.prototype.center = function center(target) { - if (_isTiledCRS(this._crs)) { + if (this.isTiledCrs()) { throw new Error('Invalid operation for WMTS bbox'); } let c; @@ -248,7 +252,7 @@ Extent.prototype.isPointInside = function isPointInside(coord, epsilon = 0) { }; Extent.prototype.isInside = function isInside(other, epsilon) { - if (_isTiledCRS(this.crs())) { + if (this.isTiledCrs()) { if (this.zoom == other.zoom) { return this.row == other.row && this.col == other.col; @@ -328,7 +332,7 @@ Extent.prototype.intersect = function intersect(other) { Extent.prototype.set = function set(...values) { - if (_isTiledCRS(this.crs())) { + if (this.isTiledCrs()) { this.zoom = values[0]; this.row = values[1]; this.col = values[2]; @@ -398,4 +402,29 @@ Extent.fromBox3 = function fromBox3(crs, box) { }); }; +Extent.prototype.extentParent = function extentParent(levelParent) { + if (this.isTiledCrs()) { + if (levelParent && levelParent < this.zoom) { + const diffLevel = this.zoom - levelParent; + const diff = Math.pow(2, diffLevel); + const invDiff = 1 / diff; + + const r = (this.row - (this.row % diff)) * invDiff; + const c = (this.col - (this.col % diff)) * invDiff; + + return new Extent(this.crs(), levelParent, r, c); + } else { + return this; + } + } +}; + +Extent.prototype.toString = function toString(separator = '') { + if (this.isTiledCrs()) { + return `${this.zoom}${separator}${this.row}${separator}${this.col}`; + } else { + return `${this.east()}${separator}${this.north()}${separator}${this.west()}${separator}${this.south()}`; + } +}; + export default Extent; diff --git a/src/Core/Prefab/Globe/GlobeLayer.js b/src/Core/Prefab/Globe/GlobeLayer.js index a8302e7719..34df898d8a 100644 --- a/src/Core/Prefab/Globe/GlobeLayer.js +++ b/src/Core/Prefab/Globe/GlobeLayer.js @@ -36,13 +36,15 @@ class GlobeLayer extends TiledGeometryLayer { * THREE.Object3d. */ constructor(id, object3d, config = {}) { - super(id, object3d || new THREE.Group(), config); + // Configure tiles + const schemeTile = globeSchemeTileWMTS(globeSchemeTile1); + const builder = new BuilderEllipsoidTile(); + super(id, object3d || new THREE.Group(), schemeTile, builder, config); this.options.defaultPickingRadius = 5; - // Configure tiles - this.schemeTile = globeSchemeTileWMTS(globeSchemeTile1); this.extent = this.schemeTile[0].clone(); + for (let i = 1; i < this.schemeTile.length; i++) { this.extent.union(this.schemeTile[i]); } @@ -52,8 +54,6 @@ class GlobeLayer extends TiledGeometryLayer { config.maxSubdivisionLevel || 18, config.sseSubdivisionThreshold || 1.0, config.maxDeltaElevationLevel || 4); - - this.builder = new BuilderEllipsoidTile(); } preUpdate(context, changeSources) { @@ -66,7 +66,7 @@ class GlobeLayer extends TiledGeometryLayer { countColorLayersTextures(...layers) { let occupancy = 0; for (const layer of layers) { - const projection = layer.projection || layer.options.projection; + const projection = layer.projection || layer.source.projection; // 'EPSG:3857' occupies the maximum 3 textures on tiles // 'EPSG:4326' occupies 1 textures on tile occupancy += projection == 'EPSG:3857' ? 3 : 1; diff --git a/src/Core/Prefab/GlobeView.js b/src/Core/Prefab/GlobeView.js index 3ba9f1259a..210731934e 100644 --- a/src/Core/Prefab/GlobeView.js +++ b/src/Core/Prefab/GlobeView.js @@ -182,8 +182,6 @@ function GlobeView(viewerDiv, coordCarto, options = {}) { }; this.addEventListener(VIEW_EVENTS.LAYERS_INITIALIZED, fn); - - this.notifyChange(this.wgs84TileLayer); } GlobeView.prototype = Object.create(View.prototype); @@ -197,7 +195,7 @@ GlobeView.prototype.addLayer = function addLayer(layer) { const colorLayerCount = this.getLayers(l => l.type === 'color').length; layer.sequence = colorLayerCount; } else if (layer.type == 'elevation') { - if (layer.protocol === 'wmts' && layer.options.tileMatrixSet !== 'WGS84G') { + if (layer.source.protocol === 'wmts' && layer.source.tileMatrixSet !== 'WGS84G') { throw new Error('Only WGS84G tileMatrixSet is currently supported for WMTS elevation layers'); } } diff --git a/src/Core/Prefab/Panorama/PanoramaLayer.js b/src/Core/Prefab/Panorama/PanoramaLayer.js index ff186a19dc..c34d337750 100644 --- a/src/Core/Prefab/Panorama/PanoramaLayer.js +++ b/src/Core/Prefab/Panorama/PanoramaLayer.js @@ -39,24 +39,10 @@ class PanoramaLayer extends TiledGeometryLayer { * THREE.Object3d. */ constructor(id, coordinates, type, config) { - super(id, config.object3d || new THREE.Group(), config); - - coordinates.xyz(this.object3d.position); - this.object3d.quaternion.setFromUnitVectors( - new THREE.Vector3(0, 0, 1), coordinates.geodesicNormal); - this.object3d.updateMatrixWorld(true); - - // FIXME: add CRS = '0' support - this.extent = new Extent('EPSG:4326', { - west: -180, - east: 180, - north: 90, - south: -90, - }); - + let schemeTile; if (type === ProjectionType.SPHERICAL) { // equirectangular -> spherical geometry - this.schemeTile = [ + schemeTile = [ new Extent('EPSG:4326', { west: -180, east: 0, @@ -70,7 +56,7 @@ class PanoramaLayer extends TiledGeometryLayer { })]; } else if (type === ProjectionType.CYLINDRICAL) { // cylindrical geometry - this.schemeTile = [ + schemeTile = [ new Extent('EPSG:4326', { west: -180, east: -90, @@ -96,6 +82,22 @@ class PanoramaLayer extends TiledGeometryLayer { throw new Error(`Unsupported panorama projection type ${type}. Only ProjectionType.SPHERICAL and ProjectionType.CYLINDRICAL are supported`); } + const builder = new PanoramaTileBuilder(type, config.ratio || 1); + super(id, config.object3d || new THREE.Group(), schemeTile, builder, config); + + coordinates.xyz(this.object3d.position); + this.object3d.quaternion.setFromUnitVectors( + new THREE.Vector3(0, 0, 1), coordinates.geodesicNormal); + this.object3d.updateMatrixWorld(true); + + // FIXME: add CRS = '0' support + this.extent = new Extent('EPSG:4326', { + west: -180, + east: 180, + north: 90, + south: -90, + }); + this.disableSkirt = true; this.culling = panoramaCulling; @@ -103,7 +105,6 @@ class PanoramaLayer extends TiledGeometryLayer { config.maxSubdivisionLevel || 10, new THREE.Vector2(512, 256)); - this.builder = new PanoramaTileBuilder(type, config.ratio || 1); this.options.segments = 8; this.options.quality = 0.5; } diff --git a/src/Core/Prefab/Planar/PlanarLayer.js b/src/Core/Prefab/Planar/PlanarLayer.js index fc7f559604..8f0a3a1663 100644 --- a/src/Core/Prefab/Planar/PlanarLayer.js +++ b/src/Core/Prefab/Planar/PlanarLayer.js @@ -35,17 +35,15 @@ class PlanarLayer extends TiledGeometryLayer { * THREE.Object3d. */ constructor(id, extent, object3d, config = {}) { - super(id, object3d || new THREE.Group(), config); + const schemeTile = [extent]; + const builder = new PlanarTileBuilder(); + super(id, object3d || new THREE.Group(), schemeTile, builder, config); this.extent = extent; - this.schemeTile = [extent]; - this.culling = planarCulling; this.subdivision = planarSubdivisionControl( config.maxSubdivisionLevel || 5, config.maxDeltaElevationLevel || 4); - - this.builder = new PlanarTileBuilder(); } } diff --git a/src/Core/Scheduler/Scheduler.js b/src/Core/Scheduler/Scheduler.js index 9235d078cb..66c801984f 100644 --- a/src/Core/Scheduler/Scheduler.js +++ b/src/Core/Scheduler/Scheduler.js @@ -5,15 +5,10 @@ */ import PriorityQueue from 'js-priority-queue'; -import WMTSProvider from '../../Provider/WMTSProvider'; -import WMSProvider from '../../Provider/WMSProvider'; +import DataSourceProvider from '../../Provider/DataSourceProvider'; import TileProvider from '../../Provider/TileProvider'; import $3dTilesProvider from '../../Provider/3dTilesProvider'; -import TMSProvider from '../../Provider/TMSProvider'; import PointCloudProvider from '../../Provider/PointCloudProvider'; -import WFSProvider from '../../Provider/WFSProvider'; -import RasterProvider from '../../Provider/RasterProvider'; -import StaticProvider from '../../Provider/StaticProvider'; import CancelledCommandException from './CancelledCommandException'; var instanceScheduler = null; @@ -132,21 +127,14 @@ Scheduler.prototype.constructor = Scheduler; Scheduler.prototype.initDefaultProviders = function initDefaultProviders() { // Register all providers - this.addProtocolProvider('wmts', WMTSProvider); - this.addProtocolProvider('wmtsc', WMTSProvider); this.addProtocolProvider('tile', TileProvider); - this.addProtocolProvider('wms', WMSProvider); this.addProtocolProvider('3d-tiles', $3dTilesProvider); - this.addProtocolProvider('tms', TMSProvider); - this.addProtocolProvider('xyz', TMSProvider); this.addProtocolProvider('potreeconverter', PointCloudProvider); - this.addProtocolProvider('wfs', WFSProvider); - this.addProtocolProvider('rasterizer', RasterProvider); - this.addProtocolProvider('static', StaticProvider); }; Scheduler.prototype.runCommand = function runCommand(command, queue, executingCounterUpToDate) { - var provider = this.providers[command.layer.protocol]; + const protocol = command.layer.protocol || command.layer.source.protocol; + const provider = this.getProtocolProvider(protocol); if (!provider) { throw new Error('No known provider for layer', command.layer.id); @@ -282,9 +270,6 @@ Scheduler.prototype.addProtocolProvider = function addProtocolProvider(protocol, if (typeof (provider.executeCommand) !== 'function') { throw new Error(`Can't add provider for ${protocol}: missing a executeCommand function.`); } - if (typeof (provider.preprocessDataLayer) !== 'function') { - throw new Error(`Can't add provider for ${protocol}: missing a preprocessDataLayer function.`); - } this.providers[protocol] = provider; }; @@ -297,7 +282,7 @@ Scheduler.prototype.addProtocolProvider = function addProtocolProvider(protocol, * @return {Provider} */ Scheduler.prototype.getProtocolProvider = function getProtocolProvider(protocol) { - return this.providers[protocol]; + return this.providers[protocol] || DataSourceProvider; }; Scheduler.prototype.commandsWaitingExecutionCount = function commandsWaitingExecutionCount() { diff --git a/src/Core/TileMesh.js b/src/Core/TileMesh.js index d00c50101a..f61290a8a8 100644 --- a/src/Core/TileMesh.js +++ b/src/Core/TileMesh.js @@ -115,12 +115,12 @@ TileMesh.prototype.setSelected = function setSelected(select) { this.material.setSelected(select); }; -TileMesh.prototype.setTextureElevation = function setTextureElevation(layer, elevation) { +TileMesh.prototype.setTextureElevation = function setTextureElevation(layer, elevation, offsetScale = new THREE.Vector4(0, 0, 1, 1)) { if (this.material === null) { return; } this.setBBoxZ(elevation.min, elevation.max); - this.material.setLayerTextures(layer, elevation); + this.material.setLayerTextures(layer, elevation.texture, offsetScale); }; @@ -141,15 +141,6 @@ TileMesh.prototype.updateGeometricError = function updateGeometricError() { this.geometricError = this.boundingSphere.radius / SIZE_TEXTURE_TILE; }; -TileMesh.prototype.setTexturesLayer = function setTexturesLayer(textures, layerType, layerId) { - if (this.material === null) { - return; - } - if (textures) { - this.material.setTexturesLayer(textures, layerType, layerId); - } -}; - TileMesh.prototype.OBB = function OBB() { return this.obb; }; @@ -172,41 +163,41 @@ TileMesh.prototype.changeSequenceLayers = function changeSequenceLayers(sequence this.material.setSequence(sequence); }; -TileMesh.prototype.getCoordsForLayer = function getCoordsForLayer(layer) { - if (layer.protocol.indexOf('wmts') == 0) { - OGCWebServiceHelper.computeTileMatrixSetCoordinates(this, layer.options.tileMatrixSet); - return this.wmtsCoords[layer.options.tileMatrixSet]; - } else if (layer.protocol == 'wms' && this.extent.crs() != layer.projection) { - if (layer.projection == 'EPSG:3857') { +TileMesh.prototype.getCoordsForSource = function getCoordsForSource(source) { + if (source.protocol.indexOf('wmts') == 0) { + OGCWebServiceHelper.computeTileMatrixSetCoordinates(this, source.tileMatrixSet); + return this.wmtsCoords[source.tileMatrixSet]; + } else if (source.protocol == 'wms' && this.extent.crs() != source.projection) { + if (source.projection == 'EPSG:3857') { const tilematrixset = 'PM'; OGCWebServiceHelper.computeTileMatrixSetCoordinates(this, tilematrixset); return this.wmtsCoords[tilematrixset]; } else { throw new Error('unsupported projection wms for this viewer'); } - } else if (layer.protocol == 'tms' || layer.protocol == 'xyz') { + } else if (source.protocol == 'tms' || source.protocol == 'xyz') { // Special globe case: use the P(seudo)M(ercator) coordinates if (is4326(this.extent.crs()) && - (layer.extent.crs() == 'EPSG:3857' || is4326(layer.extent.crs()))) { + (source.extent.crs() == 'EPSG:3857' || is4326(source.extent.crs()))) { OGCWebServiceHelper.computeTileMatrixSetCoordinates(this, 'PM'); return this.wmtsCoords.PM; } else { - return OGCWebServiceHelper.computeTMSCoordinates(this, layer.extent, layer.origin); + return OGCWebServiceHelper.computeTMSCoordinates(this, source.extent, source.origin); } - } else if (layer.extent.crs() == this.extent.crs()) { + } else if (source.extent.crs() == this.extent.crs()) { // Currently extent.as() always clone the extent, even if the output // crs is the same. // So we avoid using it if both crs are the same. return [this.extent]; } else { - return [this.extent.as(layer.extent.crs())]; + return [this.extent.as(source.extent.crs())]; } }; TileMesh.prototype.getZoomForLayer = function getZoomForLayer(layer) { - if (layer.protocol.indexOf('wmts') == 0) { - OGCWebServiceHelper.computeTileMatrixSetCoordinates(this, layer.options.tileMatrixSet); - return this.wmtsCoords[layer.options.tileMatrixSet][0].zoom; + if (layer.source.protocol.indexOf('wmts') == 0) { + OGCWebServiceHelper.computeTileMatrixSetCoordinates(this, layer.source.tileMatrixSet); + return this.wmtsCoords[layer.source.tileMatrixSet][0].zoom; } else { return this.level; } @@ -239,4 +230,15 @@ TileMesh.prototype.findCommonAncestor = function findCommonAncestor(tile) { } }; +TileMesh.prototype.findAncestorFromLevel = function fnFindAncestorFromLevel(targetLevel) { + let parentAtLevel = this; + while (parentAtLevel && parentAtLevel.level > targetLevel) { + parentAtLevel = parentAtLevel.parent; + } + if (!parentAtLevel) { + return Promise.reject(`Invalid targetLevel requested ${targetLevel}`); + } + return parentAtLevel; +}; + export default TileMesh; diff --git a/src/Core/View.js b/src/Core/View.js index 55b5efce48..c41bc9099a 100644 --- a/src/Core/View.js +++ b/src/Core/View.js @@ -13,6 +13,22 @@ import GeometryLayer from '../Layer/GeometryLayer'; import Scheduler from './Scheduler/Scheduler'; import Picking from './Picking'; +import WMTSSource from '../Source/WMTSSource'; +import WMSSource from '../Source/WMSSource'; +import WFSSource from '../Source/WFSSource'; +import TMSSource from '../Source/TMSSource'; +import StaticSource from '../Source/StaticSource'; +import FileSource from '../Source/FileSource'; + +const supportedSource = new Map([ + ['wmts', WMTSSource], + ['file', FileSource], + ['wfs', WFSSource], + ['wms', WMSSource], + ['tms', TMSSource], + ['xyz', TMSSource], + ['static', StaticSource], +]); export const VIEW_EVENTS = { /** @@ -144,24 +160,40 @@ function _preprocessLayer(view, layer, provider, parentLayer) { layer = _createLayerFromConfig(layer); } - if (provider) { - if (provider.tileInsideLimit) { - layer.tileInsideLimit = provider.tileInsideLimit.bind(provider); + if (parentLayer && !layer.extent) { + layer.extent = parentLayer.extent; + if (layer.source && !layer.source.extent) { + layer.source.extent = parentLayer.extent; } } - if (!layer.whenReady) { + if (layer.type == 'geometry' || layer.type == 'debug') { if (parentLayer || layer.type == 'debug') { // layer.threejsLayer *must* be assigned before preprocessing, // because TileProvider.preprocessDataLayer function uses it. layer.threejsLayer = view.mainLoop.gfxEngine.getUniqueThreejsLayer(); } + layer.defineLayerProperty('visible', true, () => _syncGeometryLayerVisibility(layer, view)); + _syncGeometryLayerVisibility(layer, view); + // Find projection layer, this is projection destination + layer.projection = view.referenceCrs; + } else if (layer.source.tileMatrixSet === 'PM') { + layer.projection = 'EPSG:3857'; + } else { + layer.projection = parentLayer.extent.crs(); + } + + if (!layer.whenReady) { let providerPreprocessing = Promise.resolve(); if (provider && provider.preprocessDataLayer) { providerPreprocessing = provider.preprocessDataLayer(layer, view, view.mainLoop.scheduler, parentLayer); if (!(providerPreprocessing && providerPreprocessing.then)) { providerPreprocessing = Promise.resolve(); } + } else if (layer.source) { + const protocol = layer.source.protocol; + layer.source = new (supportedSource.get(protocol))(layer.source, layer.projection); + providerPreprocessing = layer.source.whenReady || providerPreprocessing; } // the last promise in the chain must return the layer @@ -171,61 +203,10 @@ function _preprocessLayer(view, layer, provider, parentLayer) { }); } - if (layer.type == 'geometry' || layer.type == 'debug') { - layer.defineLayerProperty('visible', true, () => _syncGeometryLayerVisibility(layer, view)); - _syncGeometryLayerVisibility(layer, view); - } return layer; } -/** - * Options to wms protocol - * @typedef {Object} OptionsWms - * @property {Attribution} attribution The intellectual property rights for the layer - * @property {Object} extent Geographic extent of the service - * @property {string} name - */ - -/** - * Options to wtms protocol - * @typedef {Object} OptionsWmts - * @property {Attribution} attribution The intellectual property rights for the layer - * @property {string} attribution.name The name of the owner of the data - * @property {string} attribution.url The website of the owner of the data - * @property {string} name - * @property {string} tileMatrixSet - * @property {Array.} tileMatrixSetLimits The limits for the tile matrix set - * @property {number} tileMatrixSetLimits.minTileRow Minimum row for tiles at the level - * @property {number} tileMatrixSetLimits.maxTileRow Maximum row for tiles at the level - * @property {number} tileMatrixSetLimits.minTileCol Minimum col for tiles at the level - * @property {number} tileMatrixSetLimits.maxTileCol Maximum col for tiles at the level - * @property {Object} [zoom] - * @property {Object} [zoom.min] layer's zoom minimum - * @property {Object} [zoom.max] layer's zoom maximum - */ - -/** - * @typedef {Object} NetworkOptions - Options for fetching resources over the - * network. For json or xml fetching, this object is passed as it is to fetch - * as the init object, see [fetch documentation]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters}. - * @property {string} crossOrigin For textures, only this property is used. Its - * value is directly assigned to the crossorigin property of html tags. - * @property * Same properties as the init parameter of fetch - */ - -/** - * @typedef {Object} LayerOptions - * @property {string} id Unique layer's id - * @property {string} type the layer's type : 'color', 'elevation', 'geometry' - * @property {string} protocol wmts and wms (wmtsc for custom deprecated) - * @property {string} url Base URL of the repository or of the file(s) to load - * @property {string} format Format of this layer. See individual providers to check which formats are supported for a given layer type. - * @property {NetworkOptions} networkOptions Options for fetching resources over network - * @property {Object} updateStrategy strategy to load imagery files - * @property {OptionsWmts|OptionsWms} options WMTS or WMS options - */ - /** * Add layer in viewer. * The layer id must be unique. @@ -245,12 +226,12 @@ function _preprocessLayer(view, layer, provider, parentLayer) { * // Example to add an OPENSM Layer * view.addLayer({ * type: 'color', - * protocol: 'xyz', - * id: 'OPENSM', + * id: 'OPENSM', * fx: 2.5, - * url: 'http://b.tile.openstreetmap.fr/osmfr/${z}/${x}/${y}.png', - * format: 'image/png', - * options: { + * source: { + * protocol: 'xyz', + * url: 'http://b.tile.openstreetmap.fr/osmfr/${z}/${x}/${y}.png', + * format: 'image/png', * attribution : { * name: 'OpenStreetMap', * url: 'http://www.openstreetmap.org/', @@ -284,11 +265,8 @@ View.prototype.addLayer = function addLayer(layer, parentLayer) { return; } - if (parentLayer && !layer.extent) { - layer.extent = parentLayer.extent; - } - - const provider = this.mainLoop.scheduler.getProtocolProvider(layer.protocol); + const protocol = layer.source ? layer.source.protocol : layer.protocol; + const provider = this.mainLoop.scheduler.getProtocolProvider(protocol); if (layer.protocol && !provider) { reject(new Error(`${layer.protocol} is not a recognized protocol name.`)); return; diff --git a/src/Layer/ColorLayer.js b/src/Layer/ColorLayer.js index a2ace32bb6..a684ab1ece 100644 --- a/src/Layer/ColorLayer.js +++ b/src/Layer/ColorLayer.js @@ -1,5 +1,6 @@ import Layer from './Layer'; import { updateLayeredMaterialNodeImagery } from '../Process/LayeredMaterialNodeProcessing'; +import textureConverter from '../Parser/textureConverter'; /** * Fires when the visiblity of the layer has changed. @@ -32,13 +33,15 @@ class ColorLayer extends Layer { * contains three elements name, protocol, extent, these * elements will be available using layer.name or something * else depending on the property name. - * + * @param {WMTSSource|WMSSource|WFSSource|TMSSource|FileSource} [config.source] data source * @example * // Create a ColorLayer * const color = new ColorLayer('roads', { - * url: 'http://server.geo/wmts/SERVICE=WMTS&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL', - * protocol: 'wmts', - * format: 'image/png', + * source: { + * protocol: 'wmts', + * url: 'http://server.geo/wmts/SERVICE=WMTS&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL', + * format: 'image/png', + * } * transparent: true * }); * @@ -50,23 +53,31 @@ class ColorLayer extends Layer { * view.addLayer({ * id: 'roads', * type: 'color', - * url: 'http://server.geo/wmts/SERVICE=WMTS&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL', - * protocol: 'wmts', - * format: 'image/png', + * source: { + * url: 'http://server.geo/wmts/SERVICE=WMTS&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL', + * protocol: 'wmts', + * format: 'image/png', + * } * transparent: true * }); */ constructor(id, config = {}) { super(id, 'color', config); - + this.style = config.style || {}; this.defineLayerProperty('visible', true); this.defineLayerProperty('opacity', 1.0); this.defineLayerProperty('sequence', 0); + this.transparent = config.transparent || (this.opacity < 1.0); + this.noTextureParentOutsideLimit = config.source ? config.source.protocol == 'file' : false; } update(context, layer, node, parent) { return updateLayeredMaterialNodeImagery(context, this, node, parent); } + + convert(data, extentDestination) { + return textureConverter.convert(data, extentDestination, this); + } } export default ColorLayer; diff --git a/src/Layer/ElevationLayer.js b/src/Layer/ElevationLayer.js index 06e9b4eb2c..87a13acd1b 100644 --- a/src/Layer/ElevationLayer.js +++ b/src/Layer/ElevationLayer.js @@ -1,5 +1,6 @@ import Layer from './Layer'; import { updateLayeredMaterialNodeElevation } from '../Process/LayeredMaterialNodeProcessing'; +import textureConverter from '../Parser/textureConverter'; class ElevationLayer extends Layer { /** @@ -17,13 +18,16 @@ class ElevationLayer extends Layer { * contains three elements name, protocol, extent, these * elements will be available using layer.name or something * else depending on the property name. + * @param {WMTSSource|WMSSource|WFSSource|TMSSource|FileSource} [config.source] data source * * @example * // Create an ElevationLayer * const elevation = new ElevationLayer('IGN_MNT', { - * url: 'http://server.geo/wmts/SERVICE=WMTS&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL', - * protocol: 'wmts', - * format: 'image/x-bil;bits=32', + * source: { + * url: 'http://server.geo/wmts/SERVICE=WMTS&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL', + * protocol: 'wmts', + * format: 'image/x-bil;bits=32', + * }, * }); * * // Add the layer @@ -34,9 +38,11 @@ class ElevationLayer extends Layer { * view.addLayer({ * id: 'IGN_MNT', * type: 'elevation', - * url: 'http://server.geo/wmts/SERVICE=WMTS&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL', - * protocol: 'wmts', - * format: 'image/x-bil;bits=32', + * source: { + * url: 'http://server.geo/wmts/SERVICE=WMTS&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL', + * protocol: 'wmts', + * format: 'image/x-bil;bits=32', + * }, * }); */ constructor(id, config = {}) { @@ -46,6 +52,10 @@ class ElevationLayer extends Layer { update(context, layer, node, parent) { return updateLayeredMaterialNodeElevation(context, this, node, parent); } + + convert(data, extentDestination) { + return textureConverter.convert(data, extentDestination, this); + } } export default ElevationLayer; diff --git a/src/Layer/GeometryLayer.js b/src/Layer/GeometryLayer.js index 485f971385..5cb6b58c9f 100644 --- a/src/Layer/GeometryLayer.js +++ b/src/Layer/GeometryLayer.js @@ -25,6 +25,7 @@ class GeometryLayer extends Layer { * contains three elements name, protocol, extent, these * elements will be available using layer.name or something * else depending on the property name. + * @param {WFSSource|FileSource} [config.source] data source * * @throws {Error} object3d must be a valid * THREE.Object3d. @@ -32,9 +33,11 @@ class GeometryLayer extends Layer { * @example * // Create a GeometryLayer * const geometry = new GeometryLayer('buildings', { - * url: 'http://server.geo/wfs?', - * protocol: 'wfs', - * format: 'application/json' + * source: { + * url: 'http://server.geo/wfs?', + * protocol: 'wfs', + * format: 'application/json' + * }, * }); * * // Add the layer @@ -45,9 +48,11 @@ class GeometryLayer extends Layer { * view.addLayer({ * id: 'buildings', * type: 'geometry', - * url: 'http://server.geo/wfs?', - * protocol: 'wfs', - * format: 'application/json' + * source: { + * url: 'http://server.geo/wfs?', + * protocol: 'wfs', + * format: 'application/json' + * }, * }); */ constructor(id, object3d, config = {}) { diff --git a/src/Layer/Layer.js b/src/Layer/Layer.js index 5453866876..2d69221434 100644 --- a/src/Layer/Layer.js +++ b/src/Layer/Layer.js @@ -69,6 +69,7 @@ class Layer extends THREE.EventDispatcher { if (!this.updateStrategy) { this.updateStrategy = { type: STRATEGY_MIN_NETWORK_TRAFFIC, + options: {}, }; } @@ -91,8 +92,7 @@ class Layer extends THREE.EventDispatcher { * For example, if the added property name is frozen, it will * emit a frozen-property-changed. *

- * The emitted event has some properties assigned to it: - *
+     * @example The emitted event has some properties assigned to it
      * event = {
      *     new: {
      *         ${propertyName}: * // the new value of the property
@@ -103,7 +103,6 @@ class Layer extends THREE.EventDispatcher {
      *     target: Layer // the layer it has been dispatched from
      *     type: string // the name of the emitted event
      * }
-     * 
* * @param {string} propertyName - The name of the property, also used in * the emitted event name. diff --git a/src/Layer/LayerUpdateStrategy.js b/src/Layer/LayerUpdateStrategy.js index 32c77291d9..d3c56dc583 100644 --- a/src/Layer/LayerUpdateStrategy.js +++ b/src/Layer/LayerUpdateStrategy.js @@ -46,9 +46,9 @@ function _dichotomy(nodeLevel, currentLevel, options = {}) { Math.ceil((currentLevel + nodeLevel) / 2)); } -export function chooseNextLevelToFetch(strategy, node, nodeLevel, currentLevel, layer, failureParams) { +export function chooseNextLevelToFetch(strategy, node, nodeLevel = node.level, currentLevel, layer, failureParams) { let nextLevelToFetch; - const maxZoom = layer.options.zoom ? layer.options.zoom.max : Infinity; + const maxZoom = layer.source.zoom ? layer.source.zoom.max : Infinity; if (failureParams) { nextLevelToFetch = _dichotomy(failureParams.targetLevel, currentLevel, layer.options); } else { diff --git a/src/Layer/TiledGeometryLayer.js b/src/Layer/TiledGeometryLayer.js index f512eb290b..4e150e39d4 100644 --- a/src/Layer/TiledGeometryLayer.js +++ b/src/Layer/TiledGeometryLayer.js @@ -1,6 +1,7 @@ import GeometryLayer from './GeometryLayer'; import Picking from '../Core/Picking'; -import { processTiledGeometryNode } from '../Process/TiledNodeProcessing'; +import processTiledGeometryNode from '../Process/TiledNodeProcessing'; +import convertToTile from '../Parser/convertToTile'; class TiledGeometryLayer extends GeometryLayer { /** @@ -16,6 +17,8 @@ class TiledGeometryLayer extends GeometryLayer { * geometry of the TiledGeometryLayer. It is usually a * THREE.Group, but it can be anything inheriting from a * THREE.Object3d. + * @param {Array} schemeTile - extents Array of root tiles + * @param {Object} builder - builder geometry object * @param {Object} [config] - Optional configuration, all elements in it * will be merged as is in the layer. For example, if the configuration * contains three elements name, protocol, extent, these @@ -25,7 +28,7 @@ class TiledGeometryLayer extends GeometryLayer { * @throws {Error} object3d must be a valid * THREE.Object3d. */ - constructor(id, object3d, config) { + constructor(id, object3d, schemeTile, builder, config) { super(id, object3d, config); this.protocol = 'tile'; @@ -33,6 +36,31 @@ class TiledGeometryLayer extends GeometryLayer { enable: false, position: { x: -0.5, y: 0.0, z: 1.0 }, }; + + this.schemeTile = schemeTile; + this.builder = builder; + + if (!this.schemeTile) { + throw new Error(`Cannot init tiled layer without schemeTile for layer ${this.id}`); + } + + if (!this.builder) { + throw new Error(`Cannot init tiled layer without builder for layer ${this.id}`); + } + + this.level0Nodes = []; + const promises = []; + + for (const root of this.schemeTile) { + promises.push(this.convert(undefined, root)); + } + Promise.all(promises).then((level0s) => { + this.level0Nodes = level0s; + for (const level0 of level0s) { + this.object3d.add(level0); + level0.updateMatrixWorld(); + } + }); } /** @@ -67,7 +95,7 @@ class TiledGeometryLayer extends GeometryLayer { context.maxElevationLevel = -1; for (const e of context.elevationLayers) { - context.maxElevationLevel = Math.max(e.options.zoom.max, context.maxElevationLevel); + context.maxElevationLevel = Math.max(e.source.zoom.max, context.maxElevationLevel); } if (context.maxElevationLevel == -1) { context.maxElevationLevel = Infinity; @@ -110,6 +138,7 @@ class TiledGeometryLayer extends GeometryLayer { } onTileCreated(node) { + node.add(node.OBB()); node.material.setLightingOn(this.lighting.enable); node.material.uniforms.lightPosition.value = this.lighting.position; @@ -121,6 +150,12 @@ class TiledGeometryLayer extends GeometryLayer { node.material.uniforms.showOutline = { value: this.showOutline || false }; node.material.wireframe = this.wireframe || false; } + return node; + } + + convert(requester, extent) { + return convertToTile.convert(requester, extent, this) + .then(node => this.onTileCreated(node)); } // eslint-disable-next-line class-methods-use-this @@ -145,7 +180,8 @@ class TiledGeometryLayer extends GeometryLayer { */ static hasEnoughTexturesToSubdivide(context, node) { for (const e of context.elevationLayers) { - if (!e.frozen && e.ready && e.tileInsideLimit(node, e) && !node.material.isElevationLayerLoaded()) { + const extents = node.getCoordsForSource(e.source); + if (!e.frozen && e.ready && e.source.extentsInsideLimit(extents) && !node.material.isElevationLayerLoaded()) { // no stop subdivision in the case of a loading error if (node.layerUpdateState[e.id] && node.layerUpdateState[e.id].inError()) { continue; @@ -162,7 +198,8 @@ class TiledGeometryLayer extends GeometryLayer { if (node.layerUpdateState[c.id] && node.layerUpdateState[c.id].inError()) { continue; } - if (c.tileInsideLimit(node, c) && !node.material.isColorLayerLoaded(c)) { + const extents = node.getCoordsForSource(c.source); + if (c.source.extentsInsideLimit(extents) && !node.material.isColorLayerLoaded(c)) { return false; } } diff --git a/src/Main.js b/src/Main.js index 46e85d6fd1..52b0c5b418 100644 --- a/src/Main.js +++ b/src/Main.js @@ -17,7 +17,7 @@ export { default as GeoJsonParser } from './Parser/GeoJsonParser'; export { process3dTilesNode, init3dTilesLayer, $3dTilesCulling, $3dTilesSubdivisionControl, pre3dTilesUpdate } from './Process/3dTilesProcessing'; export { default as FeatureProcessing } from './Process/FeatureProcessing'; export { updateLayeredMaterialNodeImagery, updateLayeredMaterialNodeElevation } from './Process/LayeredMaterialNodeProcessing'; -export { processTiledGeometryNode, initTiledGeometryLayer } from './Process/TiledNodeProcessing'; +export { default as processTiledGeometryNode } from './Process/TiledNodeProcessing'; export { ColorLayersOrdering } from './Renderer/ColorLayersOrdering'; export { default as PointsMaterial } from './Renderer/PointsMaterial'; export { default as PointCloudProcessing } from './Process/PointCloudProcessing'; diff --git a/src/Parser/GeoJsonParser.js b/src/Parser/GeoJsonParser.js index 544d335c17..2a4494cf53 100644 --- a/src/Parser/GeoJsonParser.js +++ b/src/Parser/GeoJsonParser.js @@ -211,6 +211,7 @@ function readFeatures(crsIn, crsOut, features, filteringExtent, options) { res.features.push(f); } } + res.isFeature = true; return res; } diff --git a/src/Parser/VectorTileParser.js b/src/Parser/VectorTileParser.js index e0d84e0416..e5a168f469 100644 --- a/src/Parser/VectorTileParser.js +++ b/src/Parser/VectorTileParser.js @@ -4,17 +4,18 @@ import GeoJsonParser from './GeoJsonParser'; function readPBF(file, options) { const vectorTile = new VectorTile(new Protobuf(file)); - + const extentSource = options.extentSource || file.coords; const layers = Object.keys(vectorTile.layers); if (layers.length < 1) return; + const crsInId = Number(options.crsIn.slice(5)); + // We need to create a featureCollection as VectorTile does no support it const geojson = { type: 'FeatureCollection', features: [], - crs: { type: 'EPSG', properties: { code: 4326 } }, - extent: options.extent, + crs: { type: 'EPSG', properties: { code: crsInId } }, }; layers.forEach((layer_id) => { @@ -26,10 +27,10 @@ function readPBF(file, options) { // https://alastaira.wordpress.com/2011/07/06/converting-tms-tile-coordinates-to-googlebingosm-tile-coordinates/ // Only if the layer.origin is top if (options.origin == 'top') { - feature = l.feature(i).toGeoJSON(options.coords.col, options.coords.row, options.coords.zoom); + feature = l.feature(i).toGeoJSON(extentSource.col, extentSource.row, extentSource.zoom); } else { - const y = 1 << options.coords.zoom; - feature = l.feature(i).toGeoJSON(options.coords.col, y - options.coords.row - 1, options.coords.zoom); + const y = 1 << extentSource.zoom; + feature = l.feature(i).toGeoJSON(extentSource.col, y - extentSource.row - 1, extentSource.zoom); } if (layers.length > 1) { feature.properties.vt_layer = layer_id; @@ -39,20 +40,15 @@ function readPBF(file, options) { } }); - let crsOut; - switch (options.coords.crs()) { - case 'WMTS:PM': - crsOut = 'EPSG:3857'; - break; - default: - crsOut = options.extent.crs(); - } - return GeoJsonParser.parse(geojson, { - crsOut, + crsIn: options.crsIn, + crsOut: options.crsOut, filteringExtent: options.filteringExtent, filter: options.filter, buildExtent: true, + }).then((f) => { + f.extent.zoom = extentSource.zoom; + return f; }); } diff --git a/src/Parser/XbilParser.js b/src/Parser/XbilParser.js index bad890fe65..4a1485f5e2 100644 --- a/src/Parser/XbilParser.js +++ b/src/Parser/XbilParser.js @@ -1,12 +1,13 @@ -function portableXBIL(buffer) { - this.floatArray = new Float32Array(buffer); - this.max = undefined; - this.min = undefined; - this.texture = null; -} - - -export function computeMinMaxElevation(buffer, width, height, offsetScale) { + /** + * Calculates the minimum maximum elevation of xbil buffer + * + * @param {number} buffer The buffer to parse + * @param {number} width The buffer's width + * @param {number} height The buffer's height + * @param {THREE.Vector4} pitch The pitch, restrict zone to parse + * @return {Object} The minimum maximum elevation. + */ +function computeMinMaxElevation(buffer, width, height, pitch) { let min = 1000000; let max = -1000000; @@ -17,12 +18,12 @@ export function computeMinMaxElevation(buffer, width, height, offsetScale) { return { min: null, max: null }; } - const sizeX = offsetScale ? Math.floor(offsetScale.z * width) : buffer.length; - const sizeY = offsetScale ? Math.floor(offsetScale.z * height) : 1; - const xs = offsetScale ? Math.floor(offsetScale.x * width) : 0; - const ys = offsetScale ? Math.floor(offsetScale.y * height) : 0; + const sizeX = pitch ? Math.floor(pitch.z * width) : buffer.length; + const sizeY = pitch ? Math.floor(pitch.z * height) : 1; + const xs = pitch ? Math.floor(pitch.x * width) : 0; + const ys = pitch ? Math.floor(pitch.y * height) : 0; - const inc = offsetScale ? Math.max(Math.floor(sizeX / 8), 2) : 16; + const inc = pitch ? Math.max(Math.floor(sizeX / 8), 2) : 16; for (let y = ys; y < ys + sizeY; y += inc) { const pit = y * (width || 0); @@ -44,30 +45,4 @@ export function computeMinMaxElevation(buffer, width, height, offsetScale) { return { min, max }; } -export default { - /** @module XbilParser */ - /** Parse XBIL buffer and convert to portableXBIL object. - * @function parse - * @param {ArrayBuffer} buffer - the xbil buffer. - * @param {Object} options - additional properties. - * @param {string} options.url - the url from which the XBIL comes. - * @return {Promise} - a promise that resolves with a portableXBIL object. - * - */ - parse(buffer, options) { - if (!buffer) { - throw new Error('Error processing XBIL'); - } - - var result = new portableXBIL(buffer); - - var elevation = computeMinMaxElevation(result.floatArray); - - result.min = elevation.min; - result.max = elevation.max; - - result.url = options.url; - - return Promise.resolve(result); - }, -}; +export default computeMinMaxElevation; diff --git a/src/Parser/convertToTile.js b/src/Parser/convertToTile.js new file mode 100644 index 0000000000..96403b961e --- /dev/null +++ b/src/Parser/convertToTile.js @@ -0,0 +1,77 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +import * as THREE from 'three'; +import TileGeometry from '../Core/TileGeometry'; +import TileMesh from '../Core/TileMesh'; +import LayeredMaterial from '../Renderer/LayeredMaterial'; +import Cache from '../Core/Scheduler/Cache'; + +export default { + convert(requester, extent, layer) { + const builder = layer.builder; + const parent = requester; + const level = (parent !== undefined) ? (parent.level + 1) : 0; + + const { sharableExtent, quaternion, position } = builder.computeSharableExtent(extent); + const south = sharableExtent.south().toFixed(6); + const segment = layer.options.segments || 16; + const key = `${builder.type}_${layer.disableSkirt ? 0 : 1}_${segment}_${level}_${south}`; + + let geometry = Cache.get(key); + // build geometry if doesn't exist + if (!geometry) { + const paramsGeometry = { + extent: sharableExtent, + level, + segment, + disableSkirt: layer.disableSkirt, + }; + + geometry = new TileGeometry(paramsGeometry, builder); + Cache.set(key, geometry); + + geometry._count = 0; + geometry.dispose = () => { + geometry._count--; + if (geometry._count == 0) { + THREE.BufferGeometry.prototype.dispose.call(geometry); + Cache.delete(key); + } + }; + } + + // build tile + geometry._count++; + const material = new LayeredMaterial(layer.materialOptions); + const tile = new TileMesh(layer, geometry, material, extent, level); + // TODO semble ne pas etre necessaire + tile.layers.set(layer.threejsLayer); + + if (parent && parent instanceof TileMesh) { + // get parent extent transformation + const pTrans = builder.computeSharableExtent(parent.extent); + // place relative to his parent + position.sub(pTrans.position).applyQuaternion(pTrans.quaternion.inverse()); + quaternion.premultiply(pTrans.quaternion); + } + + tile.position.copy(position); + tile.quaternion.copy(quaternion); + + tile.material.transparent = layer.opacity < 1.0; + tile.material.uniforms.opacity.value = layer.opacity; + tile.setVisibility(false); + tile.updateMatrix(); + + if (parent) { + tile.setBBoxZ(parent.OBB().z.min, parent.OBB().z.max); + } else if (layer.materialOptions && layer.materialOptions.useColorTextureElevation) { + tile.setBBoxZ(layer.materialOptions.colorTextureElevationMinZ, layer.materialOptions.colorTextureElevationMaxZ); + } + + return Promise.resolve(tile); + }, +}; diff --git a/src/Parser/textureConverter.js b/src/Parser/textureConverter.js new file mode 100644 index 0000000000..758e953feb --- /dev/null +++ b/src/Parser/textureConverter.js @@ -0,0 +1,47 @@ +import * as THREE from 'three'; +import Feature2Texture from '../Renderer/ThreeExtended/Feature2Texture'; + +const textureLayer = (texture) => { + texture.generateMipmaps = false; + texture.magFilter = THREE.LinearFilter; + texture.minFilter = THREE.LinearFilter; + return texture; +}; + +function textureColorLayer(texture, transparent) { + texture.anisotropy = 16; + texture.premultiplyAlpha = transparent; + return textureLayer(texture); +} + +export default { + convert(data, extentDestination, layer) { + let texture; + if (data.isFeature) { + const backgroundColor = (layer.backgroundLayer && layer.backgroundLayer.paint) ? + new THREE.Color(layer.backgroundLayer.paint['background-color']) : + undefined; + + const extentTexture = extentDestination.as(layer.projection); + texture = Feature2Texture.createTextureFromFeature(data, extentTexture, 256, layer.style, backgroundColor); + texture.parsedData = data; + texture.coords = extentDestination; + } else if (data.isTexture) { + texture = data; + } else { + throw (new Error('Data type is not supported to convert into texture')); + } + + if (layer.type === 'color') { + return textureColorLayer(texture, layer.transparent); + } else if (layer.type === 'elevation') { + if (texture.flipY) { + // DataTexture default to false, so make sure other Texture types + // do the same (eg image texture) + // See UV construction for more details + texture.flipY = false; + } + return textureLayer(texture); + } + }, +}; diff --git a/src/Process/FeatureProcessing.js b/src/Process/FeatureProcessing.js index ebe2a5b1fa..761411fcc6 100644 --- a/src/Process/FeatureProcessing.js +++ b/src/Process/FeatureProcessing.js @@ -31,6 +31,17 @@ function applyOffset(obj, offset, quaternion, offsetAltitude) { obj.children.forEach(c => applyOffset(c, offset, quaternion, offsetAltitude)); } +function assignLayer(object, layer) { + if (object) { + object.layer = layer; + object.layers.set(layer.threejsLayer); + for (const c of object.children) { + assignLayer(c, layer); + } + return object; + } +} + const quaternion = new THREE.Quaternion(); export default { update(context, layer, node) { @@ -44,12 +55,15 @@ export default { } const features = node.children.filter(n => n.layer == layer); + // FIXME: traverse is do for each frame in each object3D + const opacity = layer.opacity === undefined ? 1.0 : layer.opacity; + const wireframe = layer.wireframe === undefined ? false : layer.wireframe; for (const feat of features) { feat.traverse((o) => { if (o.material) { - o.material.transparent = layer.opacity < 1.0; - o.material.opacity = layer.opacity; - o.material.wireframe = layer.wireframe; + o.material.transparent = opacity < 1.0; + o.material.opacity = opacity; + o.material.wireframe = wireframe; if (layer.size) { o.material.size = layer.size; @@ -64,8 +78,16 @@ export default { return features; } - if (!layer.tileInsideLimit(node, layer)) { - return; + const extentsDestination = node.getCoordsForSource(layer.source); + extentsDestination.forEach((e) => { e.zoom = node.level; }); + + const extentsSource = []; + for (const extentDest of extentsDestination) { + if (!layer.source.extentInsideLimit(extentDest) || (layer.source.parsedData && + !layer.source.parsedData.extent.isPointInside(extentDest.center()))) { + return; + } + extentsSource.push(extentDest); } if (node.layerUpdateState[layer.id] === undefined) { @@ -82,6 +104,7 @@ export default { const command = { layer, + extentsSource, view: context.view, threejsLayer: layer.threejsLayer, requester: node, @@ -89,7 +112,10 @@ export default { context.scheduler.execute(command).then((result) => { // if request return empty json, WFSProvider.getFeatures return undefined + result = result[0]; if (result) { + const isApplied = !result.layer; + assignLayer(result, layer); // call onMeshCreated callback if needed if (layer.onMeshCreated) { layer.onMeshCreated(result); @@ -102,10 +128,14 @@ export default { // We don't use node.matrixWorld here, because feature coordinates are // expressed in crs coordinates (which may be different than world coordinates, // if node's layer is attached to an Object with a non-identity transformation) - const tmp = node.extent.center().as(context.view.referenceCrs).xyz().negate(); - quaternion.setFromRotationMatrix(node.matrixWorld).inverse(); - // const quaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), node.extent.center().geodesicNormal).inverse(); - applyOffset(result, tmp, quaternion, result.minAltitude); + if (isApplied) { + // NOTE: now data source provider use cache on Mesh + const tmp = node.extent.center().as(context.view.referenceCrs).xyz().negate(); + quaternion.setFromRotationMatrix(node.matrixWorld).inverse(); + // const quaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), node.extent.center().geodesicNormal).inverse(); + applyOffset(result, tmp, quaternion, result.minAltitude); + } + if (result.minAltitude) { result.position.z = result.minAltitude; } diff --git a/src/Process/LayeredMaterialNodeProcessing.js b/src/Process/LayeredMaterialNodeProcessing.js index 4a2f927e09..8513ed4320 100644 --- a/src/Process/LayeredMaterialNodeProcessing.js +++ b/src/Process/LayeredMaterialNodeProcessing.js @@ -4,14 +4,31 @@ import LayerUpdateState from '../Layer/LayerUpdateState'; import { ImageryLayers } from '../Layer/Layer'; import CancelledCommandException from '../Core/Scheduler/CancelledCommandException'; import { SIZE_TEXTURE_TILE } from '../Provider/OGCWebServiceHelper'; -import { computeMinMaxElevation } from '../Parser/XbilParser'; +import computeMinMaxElevation from '../Parser/XbilParser'; // max retry loading before changing the status to definitiveError const MAX_RETRY = 4; +function getSourceExtent(node, extent, targetLevel, source) { + if (source && source.getSourceExtents) { + return source.getSourceExtents(extent).extent; + } else if (extent.isTiledCrs()) { + return extent.extentParent(targetLevel); + } else { + const parent = node.findAncestorFromLevel(targetLevel); + if (parent.extent) { + // Needed to initNodeElevationTextureFromParent, insertSignificantValuesFromParent, + // isColorLayerDownscaled + // Must be removed + parent.extent.zoom = parent.level; + } + return parent.extent; + } +} + function initNodeImageryTexturesFromParent(node, parent, layer) { - if (parent.material && parent.material.getColorLayerLevelById(layer.id) > EMPTY_TEXTURE_ZOOM) { - const coords = node.getCoordsForLayer(layer); + if (parent.material && parent.material.isColorLayerLoaded(layer)) { + const coords = node.getCoordsForSource(layer.source); const offsetTextures = node.material.getLayerTextureOffset(layer.id); let textureIndex = offsetTextures; @@ -44,7 +61,7 @@ function initNodeElevationTextureFromParent(node, parent, layer) { // multiple elevation layers (thus multiple calls to initNodeElevationTextureFromParent) but a given // node can only use 1 elevation texture if (parent.material && parent.material.getElevationLayerLevel() > node.material.getElevationLayerLevel()) { - const coords = node.getCoordsForLayer(layer); + const coords = node.getCoordsForSource(layer.source); const texture = parent.material.textures[l_ELEVATION][0]; const pitch = coords[0].offsetToParent(parent.material.textures[l_ELEVATION][0].coords); @@ -66,7 +83,7 @@ function initNodeElevationTextureFromParent(node, parent, layer) { elevation.max = max; } - node.setTextureElevation(layer, elevation); + node.setTextureElevation(layer, elevation, pitch); } } @@ -82,7 +99,7 @@ function getIndiceWithPitch(i, pitch, w) { function insertSignificantValuesFromParent(texture, node, parent, layer) { if (parent.material && parent.material.getElevationLayerLevel() > EMPTY_TEXTURE_ZOOM) { - const coords = node.getCoordsForLayer(layer); + const coords = node.getCoordsForSource(layer.source); const textureParent = parent.material.textures[l_ELEVATION][0]; const pitch = coords[0].offsetToParent(parent.material.textures[l_ELEVATION][0].coords); const tData = texture.image.data; @@ -140,20 +157,21 @@ function checkNodeElevationTextureValidity(texture, noDataValue) { export function updateLayeredMaterialNodeImagery(context, layer, node, parent) { const material = node.material; + const extentsDestination = node.getCoordsForSource(layer.source); // Initialisation if (node.layerUpdateState[layer.id] === undefined) { node.layerUpdateState[layer.id] = new LayerUpdateState(); - if (!layer.tileInsideLimit(node, layer)) { + if (!layer.source.extentsInsideLimit(extentsDestination)) { // we also need to check that tile's parent doesn't have a texture for this layer, // because even if this tile is outside of the layer, it could inherit it's // parent texture if (!layer.noTextureParentOutsideLimit && parent && parent.material && - parent.getIndexLayerColor && - parent.getIndexLayerColor(layer.id) >= 0) { + parent.material.indexOfColorLayer && + parent.material.indexOfColorLayer(layer.id) >= 0) { // ok, we're going to inherit our parent's texture } else { node.layerUpdateState[layer.id].noMoreUpdatePossible(); @@ -162,7 +180,7 @@ export function updateLayeredMaterialNodeImagery(context, layer, node, parent) { } if (material.indexOfColorLayer(layer.id) === -1) { - material.pushLayer(layer, node.getCoordsForLayer(layer)); + material.pushLayer(layer, node.getCoordsForSource(layer.source)); const imageryLayers = context.view.getLayers(l => l.type === 'color'); const sequence = ImageryLayers.getColorLayersIdOrderedBySequence(imageryLayers); material.setSequence(sequence); @@ -176,7 +194,7 @@ export function updateLayeredMaterialNodeImagery(context, layer, node, parent) { // The two-step allows you to filter out unnecessary requests // Indeed in the second pass, their state (not visible or not displayed) can block them to fetch - const minLevel = layer.options.zoom ? layer.options.zoom.min : 0; + const minLevel = layer.source.zoom.min; if (node.material.getColorLayerLevelById(layer.id) >= minLevel) { context.view.notifyChange(node, false); return; @@ -200,11 +218,9 @@ export function updateLayeredMaterialNodeImagery(context, layer, node, parent) { return; } - - // does this tile needs a new texture? - if (layer.canTileTextureBeImproved) { + if (layer.source.canTileTextureBeImproved) { // if the layer has a custom method -> use it - if (!layer.canTileTextureBeImproved(layer, node)) { + if (!layer.source.canTileTextureBeImproved(node.extent, material.getLayerTextures(layer)[0])) { node.layerUpdateState[layer.id].noMoreUpdatePossible(); return; } @@ -221,18 +237,24 @@ export function updateLayeredMaterialNodeImagery(context, layer, node, parent) { const failureParams = node.layerUpdateState[layer.id].failureParams; const currentLevel = node.material.getColorLayerLevelById(layer.id); - const nodeLevel = node.getCoordsForLayer(layer)[0].zoom || node.level; + const nodeLevel = extentsDestination[0].zoom || node.level; const targetLevel = chooseNextLevelToFetch(layer.updateStrategy.type, node, nodeLevel, currentLevel, layer, failureParams); if (targetLevel <= currentLevel) { return; } - // Retry tileInsideLimit because you must check with the targetLevel - // if the first test layer.tileInsideLimit returns that it is out of limits - // and the node inherits from its parent, then it'll still make a command to fetch texture. - if (!layer.tileInsideLimit(node, layer, targetLevel)) { - node.layerUpdateState[layer.id].noMoreUpdatePossible(); - return; + // Get equivalent of extent destination in source + const extentsSource = []; + for (const extentDestination of extentsDestination) { + const extentSource = getSourceExtent(node, extentDestination, targetLevel, layer.source); + if (extentSource && !layer.source.extentInsideLimit(extentSource)) { + // Retry extentInsideLimit because you must check with the targetLevel + // if the first test extentInsideLimit returns that it is out of limits + // and the node inherits from its parent, then it'll still make a command to fetch texture. + node.layerUpdateState[layer.id].noMoreUpdatePossible(); + return; + } + extentsSource.push(extentSource); } node.layerUpdateState[layer.id].newTry(); @@ -240,10 +262,12 @@ export function updateLayeredMaterialNodeImagery(context, layer, node, parent) { /* mandatory */ view: context.view, layer, + extentsSource, + extentsDestination, + parsedData: node.material.getLayerTextures(layer)[0].parsedData, requester: node, priority: nodeCommandQueuePriorityFunction(node), earlyDropFunction: refinementCommandCancellationFn, - targetLevel, }; return context.scheduler.execute(command).then( @@ -252,10 +276,9 @@ export function updateLayeredMaterialNodeImagery(context, layer, node, parent) { return; } - if (Array.isArray(result)) { - node.material.setLayerTextures(layer, result); - } else if (result.texture) { - node.material.setLayerTextures(layer, [result]); + if (result) { + const pitchs = extentsDestination.map((ext, i) => ext.offsetToParent(extentsSource[i])); + node.material.setLayerTextures(layer, result, pitchs); } else { // TODO: null texture is probably an error // Maybe add an error counter for the node/layer, @@ -300,7 +323,7 @@ export function updateLayeredMaterialNodeElevation(context, layer, node, parent) node.layerUpdateState[layer.id] = new LayerUpdateState(); initNodeElevationTextureFromParent(node, parent, layer); currentElevation = material.getElevationLayerLevel(); - const minLevel = layer.options.zoom ? layer.options.zoom.min : 0; + const minLevel = layer.source.zoom.min; if (currentElevation >= minLevel) { context.view.notifyChange(node, false); return; @@ -326,29 +349,43 @@ export function updateLayeredMaterialNodeElevation(context, layer, node, parent) } } - const c = node.getCoordsForLayer(layer)[0]; - const zoom = c.zoom || node.level; - const targetLevel = chooseNextLevelToFetch(layer.updateStrategy.type, node, zoom, currentElevation, layer); + const extentsDestination = node.getCoordsForSource(layer.source); + const targetLevel = chooseNextLevelToFetch(layer.updateStrategy.type, node, extentsDestination[0].zoom, currentElevation, layer); - if (targetLevel <= currentElevation || !layer.tileInsideLimit(node, layer, targetLevel)) { + if (targetLevel <= currentElevation) { node.layerUpdateState[layer.id].noMoreUpdatePossible(); return Promise.resolve(); } + const extentsSource = []; + for (const nodeExtent of extentsDestination) { + const extentSource = getSourceExtent(node, nodeExtent, targetLevel); + if (extentSource && !layer.source.extentInsideLimit(extentSource)) { + node.layerUpdateState[layer.id].noMoreUpdatePossible(); + return; + } + extentsSource.push(extentSource); + } + node.layerUpdateState[layer.id].newTry(); const command = { /* mandatory */ view: context.view, layer, + extentsSource, + extentsDestination, requester: node, - targetLevel, priority: nodeCommandQueuePriorityFunction(node), earlyDropFunction: refinementCommandCancellationFn, }; return context.scheduler.execute(command).then( - (terrain) => { + (textures) => { + const terrain = { texture: textures[0] }; + + // TODO: this check is maybe useless + // see in dataSourceProvider if (node.material === null) { return; } @@ -363,14 +400,6 @@ export function updateLayeredMaterialNodeElevation(context, layer, node, parent) node.layerUpdateState[layer.id].success(); - if (terrain.texture && terrain.texture.flipY) { - // DataTexture default to false, so make sure other Texture types - // do the same (eg image texture) - // See UV construction for more details - terrain.texture.flipY = false; - terrain.texture.needsUpdate = true; - } - if (terrain.texture && terrain.texture.image.data && !checkNodeElevationTextureValidity(terrain.texture, layer.noDataValue)) { // Quick check to avoid using elevation texture with no data value // If we have no data values, we use value from the parent tile @@ -378,7 +407,12 @@ export function updateLayeredMaterialNodeElevation(context, layer, node, parent) insertSignificantValuesFromParent(terrain.texture, node, parent, layer); } - node.setTextureElevation(layer, terrain); + if (terrain.texture && terrain.texture.image.data) { + const { min, max } = computeMinMaxElevation(terrain.texture.image.data); + terrain.min = !min ? 0 : min; + terrain.max = !max ? 0 : max; + } + node.setTextureElevation(layer, terrain, extentsDestination[0].offsetToParent(extentsSource[0])); }, (err) => { if (err instanceof CancelledCommandException) { diff --git a/src/Process/TiledNodeProcessing.js b/src/Process/TiledNodeProcessing.js index 3fe0ab30be..e0b02ad6d0 100644 --- a/src/Process/TiledNodeProcessing.js +++ b/src/Process/TiledNodeProcessing.js @@ -25,44 +25,24 @@ function subdivisionExtents(bbox) { return [northWest, northEast, southWest, southEast]; } -export function requestNewTile(view, scheduler, geometryLayer, extent, parent, level) { - const command = { - /* mandatory */ - view, - requester: parent, - layer: geometryLayer, - priority: 10000, - /* specific params */ - extent, - level, - redraw: false, - threejsLayer: geometryLayer.threejsLayer, - }; - - return scheduler.execute(command).then((node) => { - node.add(node.OBB()); - geometryLayer.onTileCreated(node); - return node; - }); -} - function subdivideNode(context, layer, node) { if (!node.pendingSubdivision && !node.children.some(n => n.layer == layer)) { const extents = subdivisionExtents(node.extent); // TODO: pendingSubdivision mechanism is fragile, get rid of it node.pendingSubdivision = true; - const promises = []; - const children = []; - for (const extent of extents) { - promises.push( - requestNewTile(context.view, context.scheduler, layer, extent, node).then((child) => { - children.push(child); - return node; - })); - } - - Promise.all(promises).then(() => { + const command = { + /* mandatory */ + view: context.view, + requester: node, + layer, + priority: 10000, + /* specific params */ + extentsSource: extents, + redraw: false, + }; + + context.scheduler.execute(command).then((children) => { for (const child of children) { node.add(child); child.updateMatrixWorld(true); @@ -92,7 +72,7 @@ function subdivideNode(context, layer, node) { } } -export function processTiledGeometryNode(cullingTest, subdivisionTest) { +export default function processTiledGeometryNode(cullingTest, subdivisionTest) { return function _processTiledGeometryNode(context, layer, node) { if (!node.parent) { return ObjectRemovalHelper.removeChildrenAndCleanup(layer, node); diff --git a/src/Provider/DataSourceProvider.js b/src/Provider/DataSourceProvider.js new file mode 100644 index 0000000000..703634a463 --- /dev/null +++ b/src/Provider/DataSourceProvider.js @@ -0,0 +1,131 @@ +import GeoJsonParser from '../Parser/GeoJsonParser'; +import VectorTileParser from '../Parser/VectorTileParser'; +import Fetcher from './Fetcher'; +import Cache from '../Core/Scheduler/Cache'; +import CancelledCommandException from '../Core/Scheduler/CancelledCommandException'; + +export const supportedFetchers = new Map([ + ['image/png', Fetcher.texture], + ['image/jpg', Fetcher.texture], + ['image/jpeg', Fetcher.texture], + ['image/x-bil;bits=32', Fetcher.textureFloat], + ['geojson', Fetcher.json], + ['application/json', Fetcher.json], + ['application/json', Fetcher.json], + ['application/x-protobuf;type=mapbox-vector', Fetcher.arrayBuffer], +]); + +function noParsingNeeded(data) { + return data; +} + +const supportedParsers = new Map([ + ['geojson', GeoJsonParser.parse], + ['application/json', GeoJsonParser.parse], + ['application/x-protobuf;type=mapbox-vector', VectorTileParser.parse], + [true, noParsingNeeded], +]); + +function isValidData(data, extentDestination, validFn) { + if (data && (!validFn || validFn(data, extentDestination))) { + return data; + } +} + +function fetchData(url, format, networkOptions, extentSource) { + const fetcher = supportedFetchers.get(format); + if (fetcher) { + return fetcher(url, networkOptions).then((d) => { + d.coords = extentSource; + return d; + }); + } else { + throw new Error('Not supported format, not found fetcher in DataSourceProvider.supportedFetchers'); + } +} + +function parseData(data, layer, extentDestination) { + const type = data.isTexture || data.isFeature || layer.source.format; + const options = { + buildExtent: true, + crsIn: layer.source.projection, + crsOut: layer.projection, + // TODO FIXME: error in filtering vector tile + // filteringExtent: extentDestination.as(layer.projection), + filteringExtent: layer.type === 'geometry' ? extentDestination : undefined, + filter: layer.filter, + origin: layer.source.origin, + }; + return supportedParsers.get(type)(data, options); +} + +function FetchAndConvertSourceData(url, layer, extentSource, extentDestination) { + const source = layer.source; + // Fetch data + return fetchData(url, source.format, source.networkOptions, extentSource) + .then(fetchedData => + // Parse fetched data, it parses file to itowns's object + parseData(fetchedData, layer, extentDestination)) + .then(parsedData => + // Convert parsed data, it converts itowns's object to THREE's object + layer.convert(parsedData, extentDestination), + (err) => { + source.handlingError(err, url); + throw err; + }); +} + +export default { + executeCommand(command) { + const promises = []; + const layer = command.layer; + const source = layer.source; + const requester = command.requester; + const extentsSource = command.extentsSource; + const extentsDestination = command.extentsDestination || extentsSource; + + // TODO: Find best place to cancel Command + if (requester && + !requester.material) { + // request has been deleted + return Promise.reject(new CancelledCommandException(command)); + } + + for (let i = 0, max = extentsSource.length; i < max; i++) { + const extSource = extentsSource[i]; + const extDest = extentsDestination[i]; + + // If source, we must fetch and convert data + // URL of the resource you want to fetch + const url = source.urlFromExtent(extSource); + + // Already fetched and parsed data that can be used + const validedParsedData = isValidData(command.parsedData, extDest, layer.isValidData) || source.parsedData; + + // Tag to Cache data + const tag = validedParsedData ? `${url},${extDest.toString(',')}` : url; + + // Get converted source data, in cache + let convertedSourceData = Cache.get(tag); + + // If data isn't in cache + if (!convertedSourceData) { + if (validedParsedData) { + // Use parsed data + convertedSourceData = layer.convert(validedParsedData, extDest, layer); + } else { + // Fetch and convert + convertedSourceData = FetchAndConvertSourceData(url, layer, extSource, extDest); + } + // Put converted data in cache + Cache.set(tag, convertedSourceData, Cache.POLICIES.TEXTURE); + } + + // Verify some command is resolved + // See old WFSProvider : command.resolve(result) + promises.push(convertedSourceData); + } + + return Promise.all(promises); + }, +}; diff --git a/src/Provider/Fetcher.js b/src/Provider/Fetcher.js index 79491332dd..093d69ff47 100644 --- a/src/Provider/Fetcher.js +++ b/src/Provider/Fetcher.js @@ -1,7 +1,7 @@ -import { TextureLoader } from 'three'; +import { TextureLoader, DataTexture, AlphaFormat, FloatType } from 'three'; const textureLoader = new TextureLoader(); - +const SIZE_TEXTURE_TILE = 256; function checkResponse(response) { if (!response.ok) { var error = new Error(`Error loading ${response.url}: status ${response.status}`); @@ -9,6 +9,16 @@ function checkResponse(response) { throw error; } } +const arrayBuffer = (url, options = {}) => fetch(url, options).then((response) => { + checkResponse(response); + return response.arrayBuffer(); +}); + +const getTextureFloat = function getTextureFloat(buffer) { + const texture = new DataTexture(buffer, SIZE_TEXTURE_TILE, SIZE_TEXTURE_TILE, AlphaFormat, FloatType); + texture.needsUpdate = true; + return texture; +}; export default { @@ -81,7 +91,6 @@ export default { textureLoader.load(url, res, () => {}, rej); return promise; }, - /** * Wrapper over fetch to get some ArrayBuffer * @@ -90,10 +99,12 @@ export default { * * @return {Promise} */ - arrayBuffer(url, options = {}) { - return fetch(url, options).then((response) => { - checkResponse(response); - return response.arrayBuffer(); + arrayBuffer, + textureFloat(url, options = {}) { + return arrayBuffer(url, options).then((buffer) => { + const floatArray = new Float32Array(buffer); + const texture = getTextureFloat(floatArray); + return texture; }); }, }; diff --git a/src/Provider/OGCWebServiceHelper.js b/src/Provider/OGCWebServiceHelper.js index aa8a264775..405a39a9be 100644 --- a/src/Provider/OGCWebServiceHelper.js +++ b/src/Provider/OGCWebServiceHelper.js @@ -1,48 +1,11 @@ -import * as THREE from 'three'; -import Fetcher from './Fetcher'; -import Cache from '../Core/Scheduler/Cache'; -import XbilParser from '../Parser/XbilParser'; import Projection from '../Core/Geographic/Projection'; import Extent from '../Core/Geographic/Extent'; export const SIZE_TEXTURE_TILE = 256; -const getTextureFloat = function getTextureFloat(buffer) { - const texture = new THREE.DataTexture(buffer, SIZE_TEXTURE_TILE, SIZE_TEXTURE_TILE, THREE.AlphaFormat, THREE.FloatType); - texture.needsUpdate = true; - return texture; -}; - const tileCoord = new Extent('WMTS:WGS84G', 0, 0, 0); export default { - getColorTextureByUrl(url, networkOptions) { - return Cache.get(url) || Cache.set(url, Fetcher.texture(url, networkOptions) - .then((texture) => { - texture.generateMipmaps = false; - texture.magFilter = THREE.LinearFilter; - texture.minFilter = THREE.LinearFilter; - texture.anisotropy = 16; - return texture; - }), Cache.POLICIES.TEXTURE); - }, - getXBilTextureByUrl(url, networkOptions) { - return Cache.get(url) || Cache.set(url, Fetcher.arrayBuffer(url, networkOptions) - .then(buffer => XbilParser.parse(buffer, { url })) - .then((result) => { - // TODO RGBA is needed for navigator with no support in texture float - // In RGBA elevation texture LinearFilter give some errors with nodata value. - // need to rewrite sample function in shader - - const texture = getTextureFloat(result.floatArray); - texture.generateMipmaps = false; - texture.magFilter = THREE.LinearFilter; - texture.minFilter = THREE.LinearFilter; - texture.min = result.min; - texture.max = result.max; - return texture; - }), Cache.POLICIES.ELEVATION); - }, computeTileMatrixSetCoordinates(tile, tileMatrixSet) { tileMatrixSet = tileMatrixSet || 'WGS84G'; if (!(tileMatrixSet in tile.wmtsCoords)) { @@ -66,9 +29,7 @@ export default { // See link below for more information // https://alastaira.wordpress.com/2011/07/06/converting-tms-tile-coordinates-to-googlebingosm-tile-coordinates/ computeTMSCoordinates(tile, extent, origin = 'bottom') { - if (tile.extent.crs() != extent.crs()) { - throw new Error('Unsupported configuration. TMS is only supported when geometry has the same crs than TMS layer'); - } + extent = tile.extent.crs() == extent.crs() ? extent : extent.as(tile.extent.crs()); const c = tile.extent.center(); const layerDimension = extent.dimensions(); @@ -89,21 +50,4 @@ export default { return [new Extent('TMS', zoom, Math.floor(y * tileCount), Math.floor(x * tileCount))]; }, - WMTS_WGS84Parent(cWMTS, levelParent, pitch, target = new Extent(cWMTS.crs(), 0, 0, 0)) { - const diffLevel = cWMTS.zoom - levelParent; - const diff = Math.pow(2, diffLevel); - const invDiff = 1 / diff; - - const r = (cWMTS.row - (cWMTS.row % diff)) * invDiff; - const c = (cWMTS.col - (cWMTS.col % diff)) * invDiff; - - if (pitch) { - pitch.x = cWMTS.col * invDiff - c; - pitch.y = cWMTS.row * invDiff - r; - pitch.z = invDiff; - pitch.w = invDiff; - } - - return target.set(levelParent, r, c); - }, }; diff --git a/src/Provider/RasterProvider.js b/src/Provider/RasterProvider.js deleted file mode 100644 index 38c734c35d..0000000000 --- a/src/Provider/RasterProvider.js +++ /dev/null @@ -1,130 +0,0 @@ -/** - * Class: RasterProvider - * Description: Provides textures from a vector data - */ - - -import * as THREE from 'three'; -import togeojson from 'togeojson'; -import Extent from '../Core/Geographic/Extent'; -import Feature2Texture from '../Renderer/ThreeExtended/Feature2Texture'; -import GeoJsonParser from '../Parser/GeoJsonParser'; -import Fetcher from './Fetcher'; - -function getExtentFromGpxFile(file) { - const bound = file.getElementsByTagName('bounds')[0]; - if (bound) { - const west = bound.getAttribute('minlon'); - const east = bound.getAttribute('maxlon'); - const south = bound.getAttribute('minlat'); - const north = bound.getAttribute('maxlat'); - return new Extent('EPSG:4326', west, east, south, north); - } - return new Extent('EPSG:4326', -180, 180, -90, 90); -} - -function createTextureFromVector(tile, layer) { - if (!tile.material) { - return Promise.resolve(); - } - - if (layer.type == 'color') { - const coords = tile.extent; - const result = { pitch: new THREE.Vector4(0, 0, 1, 1) }; - result.texture = Feature2Texture.createTextureFromFeature(layer.feature, tile.extent, 256, layer.style); - result.texture.extent = tile.extent; - result.texture.coords = coords; - result.texture.coords.zoom = tile.level; - - if (layer.transparent) { - result.texture.premultiplyAlpha = true; - } - return Promise.resolve(result); - } else { - return Promise.resolve(); - } -} - -export default { - preprocessDataLayer(layer, view, scheduler, parentLayer) { - if (!layer.url) { - throw new Error('layer.url is required'); - } - - // KML and GPX specifications all says that they should be in - // EPSG:4326. We still support reprojection for them through this - // configuration option - layer.projection = layer.projection || 'EPSG:4326'; - const parentCrs = parentLayer.extent.crs(); - - if (!(layer.extent instanceof Extent)) { - layer.extent = new Extent(layer.projection, layer.extent).as(parentCrs); - } - - if (!layer.options.zoom) { - layer.options.zoom = { min: 5, max: 21 }; - } - - layer.style = layer.style || {}; - - // Rasterization of data vector - // It shouldn't use parent's texture outside its extent - // Otherwise artefacts appear at the outer edge - layer.noTextureParentOutsideLimit = true; - - return Fetcher.text(layer.url, layer.networkOptions).then((text) => { - let geojson; - const trimmedText = text.trim(); - // We test the start of the string to choose a parser - if (trimmedText.startsWith('<')) { - // if it's an xml file, then it can be kml or gpx - const parser = new DOMParser(); - const file = parser.parseFromString(text, 'application/xml'); - if (file.documentElement.tagName.toLowerCase() === 'kml') { - geojson = togeojson.kml(file); - } else if (file.documentElement.tagName.toLowerCase() === 'gpx') { - geojson = togeojson.gpx(file); - layer.style.stroke = layer.style.stroke || 'red'; - layer.extent = layer.extent.intersect(getExtentFromGpxFile(file).as(layer.extent.crs())); - } else if (file.documentElement.tagName.toLowerCase() === 'parsererror') { - throw new Error('Error parsing XML document'); - } else { - throw new Error('Unsupported xml file, only valid KML and GPX are supported, but no or tag found.', - file); - } - } else if (trimmedText.startsWith('{') || trimmedText.startsWith('[')) { - geojson = JSON.parse(text); - if (geojson.type !== 'Feature' && geojson.type !== 'FeatureCollection') { - throw new Error('This json is not a GeoJSON'); - } - } else { - throw new Error('Unsupported file: only well-formed KML, GPX or GeoJSON are supported'); - } - - if (geojson) { - const options = { - buildExtent: true, - crsIn: layer.projection, - crsOut: parentCrs, - filteringExtent: layer.extent, - }; - - return GeoJsonParser.parse(geojson, options); - } - }).then((feature) => { - if (feature) { - layer.feature = feature; - layer.extent = feature.extent; - } - }); - }, - tileInsideLimit(tile, layer) { - return tile.level >= layer.options.zoom.min && tile.level <= layer.options.zoom.max && layer.extent.intersectsExtent(tile.extent); - }, - executeCommand(command) { - const layer = command.layer; - const tile = command.requester; - - return createTextureFromVector(tile, layer); - }, -}; diff --git a/src/Provider/StaticProvider.js b/src/Provider/StaticProvider.js deleted file mode 100644 index 22687fc417..0000000000 --- a/src/Provider/StaticProvider.js +++ /dev/null @@ -1,182 +0,0 @@ -import flatbush from 'flatbush'; -import { Vector4 } from 'three'; -import Extent from '../Core/Geographic/Extent'; -import OGCWebServiceHelper from './OGCWebServiceHelper'; -import Fetcher from './Fetcher'; - -function _selectImagesFromSpatialIndex(index, images, extent) { - return index.search( - extent.west(), extent.south(), - extent.east(), extent.north()).map(i => images[i]); -} - -// select the smallest image entirely covering the tile -function selectBestImageForExtent(layer, extent) { - const candidates = - _selectImagesFromSpatialIndex( - layer._spatialIndex, layer.images, extent.as(layer.extent.crs())); - - let selection; - for (const entry of candidates) { - if (extent.isInside(entry.extent)) { - if (!selection) { - selection = entry; - } else { - const d = selection.extent.dimensions(); - const e = entry.extent.dimensions(); - if (e.x <= d.x && e.y <= d.y) { - selection = entry; - } - } - } - } - return selection; -} - -function buildUrl(layer, image) { - return layer.url.href.substr(0, layer.url.href.lastIndexOf('/') + 1) - + image; -} - -function getTexture(tile, layer, targetLevel) { - if (!tile.material) { - return Promise.resolve(); - } - - if (!layer.images) { - return Promise.reject(); - } - - const original = tile; - if (targetLevel) { - while (tile && tile.level > targetLevel) { - tile = tile.parent; - } - if (!tile) { - return Promise.reject(`Invalid targetLevel requested ${targetLevel}`); - } - } - - const selection = selectBestImageForExtent(layer, tile.extent); - - if (!selection) { - return Promise.reject( - new Error(`No available image for tile ${tile}`)); - } - - - const fn = layer.format.indexOf('image/x-bil') === 0 ? - OGCWebServiceHelper.getXBilTextureByUrl : - OGCWebServiceHelper.getColorTextureByUrl; - return fn(buildUrl(layer, selection.image), layer.networkOptions).then((texture) => { - // adjust pitch - const result = { - texture, - pitch: new Vector4(0, 0, 1, 1), - }; - - result.texture.extent = selection.extent; - result.texture.coords = selection.extent; - if (!result.texture.coords.zoom || result.texture.coords.zoom > tile.level) { - result.texture.coords.zoom = tile.level; - result.texture.file = selection.image; - } - - result.pitch = original.extent.offsetToParent(selection.extent); - if (layer.transparent) { - texture.premultiplyAlpha = true; - } - - return result; - }); -} - - -/** - * This provider uses no protocol but instead download static images directly. - * - * It uses as input 'image_filename: extent' values and then tries to find the best image - * for a given tile using the extent property. - */ -export default { - preprocessDataLayer(layer) { - if (!layer.extent) { - throw new Error('layer.extent is required'); - } - - if (!(layer.extent instanceof Extent)) { - layer.extent = new Extent(layer.projection, ...layer.extent); - } - - layer.canTileTextureBeImproved = this.canTileTextureBeImproved; - layer.url = new URL(layer.url, window.location); - return Fetcher.json(layer.url.href).then((metadata) => { - layer.images = []; - // eslint-disable-next-line guard-for-in - for (const image in metadata) { - const extent = new Extent(layer.projection, ...metadata[image]); - layer.images.push({ - image, - extent, - }); - } - layer._spatialIndex = flatbush(layer.images.length); - for (const image of layer.images) { - layer._spatialIndex.add( - image.extent.west(), - image.extent.south(), - image.extent.east(), - image.extent.north()); - } - layer._spatialIndex.finish(); - }).then(() => { - if (!layer.format) { - // fetch the first image to detect format - if (layer.images.length) { - const url = buildUrl(layer, layer.images[0].image); - return fetch(url, layer.networkOptions).then((response) => { - layer.format = response.headers.get('Content-type'); - if (layer.format === 'application/octet-stream') { - layer.format = 'image/x-bil'; - } - if (!layer.format) { - throw new Error(`${layer.name}: could not detect layer format, please configure 'layer.format'.`); - } - }); - } - } - }); - }, - - tileInsideLimit(tile, layer) { - if (!layer.images) { - return false; - } - - return _selectImagesFromSpatialIndex( - layer._spatialIndex, layer.images, tile.extent.as(layer.extent.crs())).length > 0; - }, - - canTileTextureBeImproved(layer, tile) { - if (!layer.images) { - return false; - } - const s = selectBestImageForExtent(layer, tile.extent); - - if (!s) { - return false; - } - const mat = tile.material; - const currentTexture = mat.getLayerTextures(layer)[0]; - if (!currentTexture.file) { - return true; - } - return currentTexture.file != s.image; - }, - - executeCommand(command) { - const tile = command.requester; - const layer = command.layer; - return getTexture(tile, layer, command.targetLevel); - }, -}; diff --git a/src/Provider/TMSProvider.js b/src/Provider/TMSProvider.js deleted file mode 100644 index 4d7c4b04a8..0000000000 --- a/src/Provider/TMSProvider.js +++ /dev/null @@ -1,82 +0,0 @@ -import * as THREE from 'three'; -import OGCWebServiceHelper from './OGCWebServiceHelper'; -import URLBuilder from './URLBuilder'; -import Extent from '../Core/Geographic/Extent'; -import VectorTileHelper from './VectorTileHelper'; - -function preprocessDataLayer(layer) { - if (!layer.extent) { - // default to the full 3857 extent - layer.extent = new Extent('EPSG:3857', - -20037508.342789244, 20037508.342789244, - -20037508.342789255, 20037508.342789244); - } - if (!(layer.extent instanceof (Extent))) { - if (!layer.projection) { - throw new Error(`Missing projection property for layer '${layer.id}'`); - } - layer.extent = new Extent(layer.projection, ...layer.extent); - } - layer.origin = layer.origin || (layer.protocol == 'xyz' ? 'top' : 'bottom'); - if (!layer.options.zoom) { - layer.options.zoom = { - min: 0, - max: 18, - }; - } - layer.fx = layer.fx || 0.0; -} - -function executeCommand(command) { - const layer = command.layer; - const tile = command.requester; - - const promises = []; - for (const coordTMS of tile.getCoordsForLayer(layer)) { - const coordTMSParent = (command.targetLevel < coordTMS.zoom) ? - OGCWebServiceHelper.WMTS_WGS84Parent(coordTMS, command.targetLevel) : - undefined; - const urld = URLBuilder.xyz(coordTMSParent || coordTMS, layer); - - const promise = layer.format === 'application/x-protobuf;type=mapbox-vector' ? - VectorTileHelper.getVectorTileTextureByUrl(urld, tile, layer, coordTMS) : - OGCWebServiceHelper.getColorTextureByUrl(urld, layer.networkOptions); - - promises.push(promise.then((texture) => { - const result = {}; - result.texture = texture; - result.texture.coords = coordTMSParent || coordTMS; - result.pitch = coordTMSParent ? - coordTMS.offsetToParent(coordTMSParent) : - new THREE.Vector4(0, 0, 1, 1); - if (layer.transparent) { - texture.premultiplyAlpha = true; - } - return result; - })); - } - return Promise.all(promises); -} - -function tileTextureCount(tile, layer) { - return tileInsideLimit(tile, layer) ? 1 : 0; -} - -function tileInsideLimit(tile, layer, targetLevel) { - // assume 1 TMS texture per tile (ie: tile geometry CRS is the same as layer's CRS) - let tmsCoord = tile.getCoordsForLayer(layer)[0]; - - if (targetLevel < tmsCoord.zoom) { - tmsCoord = OGCWebServiceHelper.WMTS_WGS84Parent(tmsCoord, targetLevel); - } - - return layer.options.zoom.min <= tmsCoord.zoom && - tmsCoord.zoom <= layer.options.zoom.max; -} - -export default { - preprocessDataLayer, - executeCommand, - tileTextureCount, - tileInsideLimit, -}; diff --git a/src/Provider/TileProvider.js b/src/Provider/TileProvider.js index 320be8e05a..294a68c0a8 100644 --- a/src/Provider/TileProvider.js +++ b/src/Provider/TileProvider.js @@ -1,110 +1,21 @@ -/* - * To change this license header, choose License Headers in Project Properties. - * To change this template file, choose Tools | Templates - * and open the template in the editor. - */ -import * as THREE from 'three'; -import TileGeometry from '../Core/TileGeometry'; -import TileMesh from '../Core/TileMesh'; -import LayeredMaterial from '../Renderer/LayeredMaterial'; import CancelledCommandException from '../Core/Scheduler/CancelledCommandException'; -import Cache from '../Core/Scheduler/Cache'; -import { requestNewTile } from '../Process/TiledNodeProcessing'; -function preprocessDataLayer(layer, view, scheduler) { - if (!layer.schemeTile) { - throw new Error(`Cannot init tiled layer without schemeTile for layer ${layer.id}`); - } - - layer.level0Nodes = []; - layer.onTileCreated = layer.onTileCreated || (() => {}); - - const promises = []; - - for (const root of layer.schemeTile) { - promises.push(requestNewTile(view, scheduler, layer, root, undefined, 0)); - } - return Promise.all(promises).then((level0s) => { - layer.level0Nodes = level0s; - for (const level0 of level0s) { - layer.object3d.add(level0); - level0.updateMatrixWorld(); +export default { + executeCommand(command) { + const promises = []; + const layer = command.layer; + const requester = command.requester; + const extentsSource = command.extentsSource; + + if (requester && + !requester.material) { + return Promise.reject(new CancelledCommandException(command)); } - }); -} - -function executeCommand(command) { - const extent = command.extent; - if (command.requester && - !command.requester.material) { - // request has been deleted - return Promise.reject(new CancelledCommandException(command)); - } - const layer = command.layer; - const builder = layer.builder; - const parent = command.requester; - const level = (command.level === undefined) ? (parent.level + 1) : command.level; - - const { sharableExtent, quaternion, position } = builder.computeSharableExtent(extent); - const south = sharableExtent.south().toFixed(6); - const segment = layer.options.segments || 16; - const key = `${builder.type}_${layer.disableSkirt ? 0 : 1}_${segment}_${level}_${south}`; - - let geometry = Cache.get(key); - // build geometry if doesn't exist - if (!geometry) { - const paramsGeometry = { - extent: sharableExtent, - level, - segment, - disableSkirt: layer.disableSkirt, - }; - - geometry = new TileGeometry(paramsGeometry, builder); - Cache.set(key, geometry); - geometry._count = 0; - geometry.dispose = () => { - geometry._count--; - if (geometry._count == 0) { - THREE.BufferGeometry.prototype.dispose.call(geometry); - Cache.delete(key); - } - }; - } - - // build tile - geometry._count++; - const material = new LayeredMaterial(layer.materialOptions); - const tile = new TileMesh(layer, geometry, material, extent, level); - tile.layers.set(command.threejsLayer); - - if (parent && parent instanceof TileMesh) { - // get parent extent transformation - const pTrans = builder.computeSharableExtent(parent.extent); - // place relative to his parent - position.sub(pTrans.position).applyQuaternion(pTrans.quaternion.inverse()); - quaternion.premultiply(pTrans.quaternion); - } - - tile.position.copy(position); - tile.quaternion.copy(quaternion); - - tile.material.transparent = layer.opacity < 1.0; - tile.material.uniforms.opacity.value = layer.opacity; - tile.setVisibility(false); - tile.updateMatrix(); - - if (parent) { - tile.setBBoxZ(parent.OBB().z.min, parent.OBB().z.max); - } else if (layer.materialOptions && layer.materialOptions.useColorTextureElevation) { - tile.setBBoxZ(layer.materialOptions.colorTextureElevationMinZ, layer.materialOptions.colorTextureElevationMaxZ); - } - - return Promise.resolve(tile); -} + for (let i = 0, size = extentsSource.length; i < size; i++) { + promises.push(layer.convert(requester, extentsSource[i])); + } -export default { - preprocessDataLayer, - executeCommand, + return Promise.all(promises); + }, }; diff --git a/src/Provider/WFSProvider.js b/src/Provider/WFSProvider.js deleted file mode 100644 index 2b98d66385..0000000000 --- a/src/Provider/WFSProvider.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Generated On: 2016-03-5 - * Class: WFSProvider - * Description: Provides data from a WFS stream - */ - -import Extent from '../Core/Geographic/Extent'; -import URLBuilder from './URLBuilder'; -import Fetcher from './Fetcher'; -import Cache from '../Core/Scheduler/Cache'; -import GeoJsonParser from '../Parser/GeoJsonParser'; -import Feature2Mesh from '../Renderer/ThreeExtended/Feature2Mesh'; - -function preprocessDataLayer(layer) { - if (!layer.typeName) { - throw new Error('layer.typeName is required.'); - } - - layer.format = layer.format || 'application/json'; - - layer.crs = layer.projection || 'EPSG:4326'; - layer.version = layer.version || '2.0.2'; - layer.opacity = layer.opacity || 1; - layer.wireframe = layer.wireframe || false; - if (!(layer.extent instanceof Extent)) { - layer.extent = new Extent(layer.projection, layer.extent); - } - layer.url = `${layer.url - }SERVICE=WFS&REQUEST=GetFeature&typeName=${layer.typeName - }&VERSION=${layer.version - }&SRSNAME=${layer.crs - }&outputFormat=${layer.format - }&BBOX=%bbox,${layer.crs}`; -} - -function tileInsideLimit(tile, layer) { - return (layer.level === undefined || tile.level === layer.level) && layer.extent.intersectsExtent(tile.extent); -} - -function executeCommand(command) { - const layer = command.layer; - const tile = command.requester; - const destinationCrs = command.view.referenceCrs; - return getFeatures(destinationCrs, tile, layer, command).then(result => command.resolve(result)); -} - -function assignLayer(object, layer) { - if (object) { - object.layer = layer; - object.layers.set(layer.threejsLayer); - for (const c of object.children) { - assignLayer(c, layer); - } - return object; - } -} - -function getFeatures(crs, tile, layer) { - if (!layer.tileInsideLimit(tile, layer) || tile.material === null) { - return Promise.resolve(); - } - - const urld = URLBuilder.bbox(tile.extent.as(layer.crs), layer); - - layer.convert = layer.convert ? layer.convert : Feature2Mesh.convert({}); - - return (Cache.get(urld) || Cache.set(urld, Fetcher.json(urld, layer.networkOptions))) - .then( - geojson => GeoJsonParser.parse(geojson, { crsOut: crs, filteringExtent: tile.extent, filter: layer.filter }), - (err) => { - // special handling for 400 errors, as it probably means the config is wrong - if (err.response.status == 400) { - return err.response.text().then((text) => { - const getCapUrl = `${layer.url}SERVICE=WFS&REQUEST=GetCapabilities&VERSION=${layer.version}`; - const xml = new DOMParser().parseFromString(text, 'application/xml'); - const errorElem = xml.querySelector('Exception'); - const errorCode = errorElem.getAttribute('exceptionCode'); - const errorMessage = errorElem.querySelector('ExceptionText').textContent; - console.error(`Layer ${layer.name}: bad request when fetching data. Server says: "${errorCode}: ${errorMessage}". \nReviewing ${getCapUrl} may help.`, err); - throw err; - }); - } else { - console.error(`Layer ${layer.name}: ${err.response.status} error while trying to fetch WFS data. Url was ${urld}.`, err); - throw err; - } - }) - .then(feature => assignLayer(layer.convert(feature), layer)); -} - -export default { - preprocessDataLayer, - executeCommand, - tileInsideLimit, -}; diff --git a/src/Provider/WMSProvider.js b/src/Provider/WMSProvider.js deleted file mode 100644 index 30ce8c2836..0000000000 --- a/src/Provider/WMSProvider.js +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Generated On: 2015-10-5 - * Class: WMSProvider - * Description: Provides data from a WMS stream - */ - -import * as THREE from 'three'; -import Extent from '../Core/Geographic/Extent'; -import OGCWebServiceHelper from './OGCWebServiceHelper'; -import URLBuilder from './URLBuilder'; - -const supportedFormats = ['image/png', 'image/jpg', 'image/jpeg']; - -function tileTextureCount(tile, layer) { - return tile.extent.crs() == layer.projection ? 1 : tile.getCoordsForLayer(layer).length; -} - -function preprocessDataLayer(layer) { - if (!layer.name) { - throw new Error('layer.name is required.'); - } - if (!layer.extent) { - throw new Error('layer.extent is required'); - } - if (!layer.projection) { - throw new Error('layer.projection is required'); - } - - if (!(layer.extent instanceof Extent)) { - layer.extent = new Extent(layer.projection, layer.extent); - } - - if (!layer.options.zoom) { - layer.options.zoom = { min: 0, max: 21 }; - } - - layer.format = layer.format || 'image/png'; - if (!supportedFormats.includes(layer.format)) { - throw new Error(`Layer ${layer.name}: unsupported format '${layer.format}', should be one of '${supportedFormats.join('\', \'')}'`); - } - - layer.width = layer.heightMapWidth || 256; - layer.version = layer.version || '1.3.0'; - layer.style = layer.style || ''; - layer.transparent = layer.transparent || false; - - if (!layer.axisOrder) { - // 4326 (lat/long) axis order depends on the WMS version used - if (layer.projection == 'EPSG:4326') { - // EPSG 4326 x = lat, long = y - // version 1.1.0 long/lat while version 1.3.0 mandates xy (so lat,long) - layer.axisOrder = (layer.version === '1.1.0' ? 'wsen' : 'swne'); - } else { - // xy,xy order - layer.axisOrder = 'wsen'; - } - } - let crsPropName = 'SRS'; - if (layer.version === '1.3.0') { - crsPropName = 'CRS'; - } - - layer.url = `${layer.url - }?SERVICE=WMS&REQUEST=GetMap&LAYERS=${layer.name - }&VERSION=${layer.version - }&STYLES=${layer.style - }&FORMAT=${layer.format - }&TRANSPARENT=${layer.transparent - }&BBOX=%bbox` + - `&${crsPropName}=${layer.projection - }&WIDTH=${layer.width - }&HEIGHT=${layer.width}`; -} - -function tileInsideLimit(tile, layer) { - return tile.level >= layer.options.zoom.min && - tile.level <= layer.options.zoom.max && - layer.extent.intersectsExtent(tile.extent); -} - -function getColorTexture(tile, layer, targetLevel, tileCoords) { - if (!tileInsideLimit(tile, layer)) { - return Promise.reject(`Tile '${tile}' is outside layer bbox ${layer.extent}`); - } - if (tile.material === null) { - return Promise.resolve(); - } - - let extent = tileCoords ? tileCoords.as(layer.projection) : tile.extent; - // if no specific level requester, use tile.level - if (targetLevel === undefined) { - targetLevel = tile.level; - } else if (!tileCoords) { - let parentAtLevel = tile; - while (parentAtLevel && parentAtLevel.level > targetLevel) { - parentAtLevel = parentAtLevel.parent; - } - if (!parentAtLevel) { - return Promise.reject(`Invalid targetLevel requested ${targetLevel}`); - } - extent = parentAtLevel.extent; - targetLevel = parentAtLevel.level; - } - - const coords = extent.as(layer.projection); - const urld = URLBuilder.bbox(coords, layer); - const pitch = tileCoords ? new THREE.Vector4(0, 0, 1, 1) : tile.extent.offsetToParent(extent); - const result = { pitch }; - - return OGCWebServiceHelper.getColorTextureByUrl(urld, layer.networkOptions).then((texture) => { - result.texture = texture; - result.texture.extent = extent; - if (layer.transparent) { - texture.premultiplyAlpha = true; - } - if (tileCoords) { - result.texture.coords = tileCoords; - } else { - result.texture.coords = coords; - // LayeredMaterial expects coords.zoom to exist, and describe the - // precision of the texture (a la WMTS). - result.texture.coords.zoom = targetLevel; - } - return result; - }); -} - -function executeCommand(command) { - const tile = command.requester; - - const layer = command.layer; - const getTextureFunction = tile.extent.crs() == layer.projection ? getColorTexture : getColorTextures; - - return getTextureFunction(tile, layer, command.targetLevel); -} - -// In the case where the tilematrixset of the tile don't correspond to the projection of the layer -// when the projection of the layer corresponds to a tilematrixset inside the tile, like the PM -function getColorTextures(tile, layer, targetLevel) { - if (tile.material === null) { - return Promise.resolve(); - } - const promises = []; - for (const coord of tile.getCoordsForLayer(layer)) { - promises.push(getColorTexture(tile, layer, targetLevel, coord)); - } - - return Promise.all(promises); -} - -export default { - preprocessDataLayer, - executeCommand, - tileTextureCount, - tileInsideLimit, -}; diff --git a/src/Provider/WMTSProvider.js b/src/Provider/WMTSProvider.js deleted file mode 100644 index 71e6019ee7..0000000000 --- a/src/Provider/WMTSProvider.js +++ /dev/null @@ -1,201 +0,0 @@ -/** - * Generated On: 2015-10-5 - * Class: WMTSProvider - * Description: Fournisseur de données à travers un flux WMTS - */ - -import * as THREE from 'three'; -import OGCWebServiceHelper, { SIZE_TEXTURE_TILE } from './OGCWebServiceHelper'; -import URLBuilder from './URLBuilder'; -import Extent from '../Core/Geographic/Extent'; -import { computeMinMaxElevation } from '../Parser/XbilParser'; - -const coordTile = new Extent('WMTS:WGS84', 0, 0, 0); - -const supportedFormats = new Map([ - ['image/png', getColorTextures], - ['image/jpg', getColorTextures], - ['image/jpeg', getColorTextures], - ['image/x-bil;bits=32', getXbilTexture], -]); - -function preprocessDataLayer(layer) { - layer.fx = layer.fx || 0.0; - - layer.format = layer.format || 'image/png'; - if (!supportedFormats.has(layer.format)) { - throw new Error( - `Layer ${layer.name}: unsupported layer.format '${layer.format}', must be one of '${Array.from(supportedFormats.keys()).join('\', \'')}'`); - } - - if (layer.protocol === 'wmts') { - const options = layer.options; - options.version = options.version || '1.0.0'; - options.tileMatrixSet = options.tileMatrixSet || 'WGS84'; - options.style = options.style || 'normal'; - // If the projection is undefined, - // It is deduced from the tileMatrixSet, - // The projection is coherent with the projection - if (!options.projection) { - if (options.tileMatrixSet === 'WGS84' || options.tileMatrixSet === 'WGS84G') { - options.projection = 'EPSG:4326'; - } else { - options.projection = 'EPSG:3857'; - } - } - let newBaseUrl = `${layer.url}` + - `?LAYER=${options.name}` + - `&FORMAT=${layer.format}` + - '&SERVICE=WMTS' + - `&VERSION=${options.version}` + - '&REQUEST=GetTile' + - `&STYLE=${options.style}` + - `&TILEMATRIXSET=${options.tileMatrixSet}`; - - newBaseUrl += '&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL'; - - if (!layer.options.zoom) { - const arrayLimits = Object.keys(options.tileMatrixSetLimits); - const size = arrayLimits.length; - const maxZoom = Number(arrayLimits[size - 1]); - const minZoom = maxZoom - size + 1; - - layer.options.zoom = { - min: minZoom, - max: maxZoom, - }; - } - layer.url = newBaseUrl; - } - layer.options.zoom = layer.options.zoom || { min: 2, max: 20 }; -} - -/** - * return texture float alpha THREE.js of MNT - * @param {TileMesh} tile - * @param {Layer} layer - * @param {number} targetZoom - * @returns {Promise} - */ -function getXbilTexture(tile, layer, targetZoom) { - const pitch = new THREE.Vector4(0.0, 0.0, 1.0, 1.0); - let coordWMTS = tile.getCoordsForLayer(layer)[0]; - - if (targetZoom && targetZoom !== coordWMTS.zoom) { - coordWMTS = OGCWebServiceHelper.WMTS_WGS84Parent(coordWMTS, targetZoom, pitch); - } - - const urld = URLBuilder.xyz(coordWMTS, layer); - - return OGCWebServiceHelper.getXBilTextureByUrl(urld, layer.networkOptions).then((texture) => { - texture.coords = coordWMTS; - let min; - let max; - - // if pitch is full texture, the min and max are already known - if (pitch.z == 1.0 && pitch.w == 1.0) { - min = texture.min; - max = texture.max; - } else { - const r = computeMinMaxElevation(texture.image.data, - SIZE_TEXTURE_TILE, SIZE_TEXTURE_TILE, - pitch); - min = r.min; - max = r.max; - } - - return { - min, - max, - texture, - pitch, - }; - }); -} - -/** - * Return texture RGBA THREE.js of orthophoto - * TODO : RGBA --> RGB remove alpha canal - * @param {{zoom:number,row:number,col:number}} coordWMTS - * @param {Layer} layer - * @param {number} targetZoom - * @returns {Promise} - */ -function getColorTexture(coordWMTS, layer, targetZoom) { - const pitch = new THREE.Vector4(0.0, 0.0, 1.0, 1.0); - if (targetZoom && targetZoom !== coordWMTS.zoom) { - coordWMTS = OGCWebServiceHelper.WMTS_WGS84Parent(coordWMTS, targetZoom, pitch); - } - - const urld = URLBuilder.xyz(coordWMTS, layer); - return OGCWebServiceHelper.getColorTextureByUrl(urld, layer.networkOptions).then((texture) => { - const result = {}; - result.texture = texture; - result.texture.coords = coordWMTS; - result.pitch = pitch; - if (layer.transparent) { - texture.premultiplyAlpha = true; - } - - return result; - }); -} - -function executeCommand(command) { - const layer = command.layer; - const tile = command.requester; - return supportedFormats.get(layer.format)(tile, layer, command.targetLevel); -} - -function tileTextureCount(tile, layer) { - const tileMatrixSet = layer.options.tileMatrixSet; - OGCWebServiceHelper.computeTileMatrixSetCoordinates(tile, tileMatrixSet); - return tile.getCoordsForLayer(layer).length; -} - -function tileInsideLimit(tile, layer, targetLevel) { - // This layer provides data starting at level = layer.options.zoom.min - // (the zoom.max property is used when building the url to make - // sure we don't use invalid levels) - for (const coord of tile.getCoordsForLayer(layer)) { - let c = coord; - // override - if (targetLevel < c.zoom) { - OGCWebServiceHelper.WMTS_WGS84Parent(coord, targetLevel, undefined, coordTile); - c = coordTile; - } - if (c.zoom < layer.options.zoom.min || c.zoom > layer.options.zoom.max) { - return false; - } - if (layer.options.tileMatrixSetLimits) { - if (c.row < layer.options.tileMatrixSetLimits[c.zoom].minTileRow || - c.row > layer.options.tileMatrixSetLimits[c.zoom].maxTileRow || - c.col < layer.options.tileMatrixSetLimits[c.zoom].minTileCol || - c.col > layer.options.tileMatrixSetLimits[c.zoom].maxTileCol) { - return false; - } - } - } - return true; -} - -function getColorTextures(tile, layer, targetZoom) { - if (tile.material === null) { - return Promise.resolve(); - } - const promises = []; - const bcoord = tile.getCoordsForLayer(layer); - - for (const coordWMTS of bcoord) { - promises.push(getColorTexture(coordWMTS, layer, targetZoom)); - } - - return Promise.all(promises); -} - -export default { - preprocessDataLayer, - executeCommand, - tileTextureCount, - tileInsideLimit, -}; diff --git a/src/Renderer/LayeredMaterial.js b/src/Renderer/LayeredMaterial.js index aee683be70..77fdad0a54 100644 --- a/src/Renderer/LayeredMaterial.js +++ b/src/Renderer/LayeredMaterial.js @@ -327,29 +327,29 @@ LayeredMaterial.prototype.removeColorLayer = function removeColorLayer(layer) { this.uniforms.dTextures_01.value = this.textures[l_COLOR]; }; -LayeredMaterial.prototype.setLayerTextures = function setLayerTextures(layer, textures) { +LayeredMaterial.prototype.setLayerTextures = function setLayerTextures(layer, textures, pitchs) { if (layer.type === 'elevation') { if (Array.isArray(textures)) { textures = textures[0]; } - this._setTexture(textures.texture, l_ELEVATION, 0, textures.pitch); + this._setTexture(textures, l_ELEVATION, 0, pitchs); } else if (layer.type === 'color') { const index = this.indexOfColorLayer(layer.id); const slotOffset = this.getTextureOffsetByLayerIndex(index); if (Array.isArray(textures)) { for (let i = 0, max = textures.length; i < max; i++) { if (textures[i]) { - if (textures[i].texture !== null) { - this._setTexture(textures[i].texture, l_COLOR, - i + (slotOffset || 0), textures[i].pitch); + if (textures[i] !== null) { + this._setTexture(textures[i], l_COLOR, + i + (slotOffset || 0), pitchs[i]); } else { this.setLayerVisibility(index, false); break; } } } - } else if (textures.texture !== null) { - this._setTexture(textures.texture, l_COLOR, (slotOffset || 0), textures.pitch); + } else if (textures !== null) { + this._setTexture(textures, l_COLOR, (slotOffset || 0), pitchs[0]); } else { this.setLayerVisibility(index, false); } @@ -476,8 +476,12 @@ LayeredMaterial.prototype.getColorLayerLevelById = function getColorLayerLevelBy LayeredMaterial.prototype.isColorLayerLoaded = function isColorLayerLoaded(layer) { const textures = this.getLayerTextures(layer); - if (textures.length) { - return textures[0].coords.zoom > EMPTY_TEXTURE_ZOOM; + if (textures[0]) { + if (textures[0].coords.zoom !== undefined) { + return textures[0].coords.zoom > EMPTY_TEXTURE_ZOOM; + } else { + return textures[0].image !== undefined; + } } return false; }; diff --git a/src/Renderer/ThreeExtended/Feature2Texture.js b/src/Renderer/ThreeExtended/Feature2Texture.js index 2bb5a01a85..8e7849b572 100644 --- a/src/Renderer/ThreeExtended/Feature2Texture.js +++ b/src/Renderer/ThreeExtended/Feature2Texture.js @@ -115,12 +115,8 @@ export default { drawFeature(ctx, feature, origin, scale, extent, style); } - texture = new THREE.Texture(c); + texture = new THREE.CanvasTexture(c); texture.flipY = false; - texture.generateMipmaps = false; - texture.magFilter = THREE.LinearFilter; - texture.minFilter = THREE.LinearFilter; - texture.needsUpdate = true; } else if (backgroundColor) { const data = new Uint8Array(3); data[0] = backgroundColor.r * 255; diff --git a/src/Source/FileSource.js b/src/Source/FileSource.js new file mode 100644 index 0000000000..dbedee3253 --- /dev/null +++ b/src/Source/FileSource.js @@ -0,0 +1,127 @@ +import togeojson from 'togeojson'; +import Source from './Source'; +import Fetcher from '../Provider/Fetcher'; +import Extent from '../Core/Geographic/Extent'; +import GeoJsonParser from '../Parser/GeoJsonParser'; + +function getExtentFromGpxFile(file) { + const bound = file.getElementsByTagName('bounds')[0]; + if (bound) { + const west = bound.getAttribute('minlon'); + const east = bound.getAttribute('maxlon'); + const south = bound.getAttribute('minlat'); + const north = bound.getAttribute('maxlat'); + return new Extent('EPSG:4326', west, east, south, north); + } + return new Extent('EPSG:4326', -180, 180, -90, 90); +} + +// TODO move and refacto +function fileParser(text) { + let parsedFile; + const trimmedText = text.trim(); + // We test the start of the string to choose a parser + if (trimmedText.startsWith('<')) { + // if it's an xml file, then it can be kml or gpx + const parser = new DOMParser(); + const file = parser.parseFromString(text, 'application/xml'); + if (file.documentElement.tagName.toLowerCase() === 'kml') { + parsedFile = togeojson.kml(file); + } else if (file.documentElement.tagName.toLowerCase() === 'gpx') { + parsedFile = togeojson.gpx(file); + const line = parsedFile.features.find(e => e.geometry.type == 'LineString'); + line.properties.stroke = 'red'; + parsedFile.extent = getExtentFromGpxFile(file); + } else if (file.documentElement.tagName.toLowerCase() === 'parsererror') { + throw new Error('Error parsing XML document'); + } else { + throw new Error('Unsupported xml file, only valid KML and GPX are supported, but no or tag found.', + file); + } + } else if (trimmedText.startsWith('{') || trimmedText.startsWith('[')) { + parsedFile = JSON.parse(text); + if (parsedFile.type !== 'Feature' && parsedFile.type !== 'FeatureCollection') { + throw new Error('This json is not a GeoJSON'); + } + } else { + throw new Error('Unsupported file: only well-formed KML, GPX or GeoJSON are supported'); + } + + return parsedFile; +} + +class FileSource extends Source { + /** + * File source to use file in {@link GeometryLayer}, {@link ColorLayer} or {@link ElevationLayer}. + * @constructor + * @extends Source + * + * @param {sourceParams} source The source + * @param {string} source.projection Data system projection, it needed to parse data + * @param {string} crsOut crd output data + * + * @example add geometry layer with geojson file + * const ariege = new itowns.GeometryLayer('ariege', new itowns.THREE.Group()); + * ariege.update = itowns.FeatureProcessing.update; + * ariege.convert = itowns.Feature2Mesh.convert({ + * color: () => new itowns.THREE.Color(0xffcc00), + * extrude: () => 5000, + * }); + * ariege.source = { + * protocol: 'file', + * url: 'https://raw.githubusercontent.com/gregoiredavid/france-geojson/master/departements/09-ariege/departement-09-ariege.geojson', + * projection: 'EPSG:4326', + * format: 'application/json', + * zoom: { min: 7, max: 7 }, + * }; + * view.addLayer(ariege); + * + * @example add color layer with geojson file + * globeView.addLayer({ + * type: 'color', + * id: 'Gpx', + * name: 'Ultra 2009', + * transparent: true, + * source: { + * url: 'https://raw.githubusercontent.com/iTowns/iTowns2-sample-data/master/ULTRA2009.gpx', + * protocol: 'file', + * projection: 'EPSG:4326', + * }, + * }); + * + */ + constructor(source, crsOut) { + if (!source.projection) { + throw new Error('source.projection is required in FileSource'); + } + super(source); + + this.url = source.url; + this.parsedData = []; + this.zoom = source.zoom || { min: 5, max: 21 }; + const options = { + buildExtent: true, + crsIn: this.projection, + crsOut, + }; + + this.whenReady = Fetcher.text(this.url, source.networkOptions).then(fileParser).then(parsedFile => + GeoJsonParser.parse(parsedFile, options).then((feature) => { + feature.style = parsedFile.style; + this.parsedData = feature; + })); + } + + urlFromExtent(extent) { + return `${this.url},${extent.crs()}`; + } + + extentInsideLimit(extent) { + const dataExtent = this.parsedData.extent; + const localExtent = this.projection == extent.crs() ? extent : extent.as(this.projection); + return (extent.zoom == undefined || !(extent.zoom < this.zoom.min || extent.zoom > this.zoom.max)) && + dataExtent.intersectsExtent(localExtent); + } +} + +export default FileSource; diff --git a/src/Source/Source.js b/src/Source/Source.js new file mode 100644 index 0000000000..20ed9adf3a --- /dev/null +++ b/src/Source/Source.js @@ -0,0 +1,98 @@ +import Extent from '../Core/Geographic/Extent'; +/** + * @typedef {Object} NetworkOptions - Options for fetching resources over the + * network. For json or xml fetching, this object is passed as it is to fetch + * as the init object, see [fetch documentation]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters}. + * @property {string} crossOrigin For textures, only this property is used. Its + * value is directly assigned to the crossorigin property of html tags. + * @property * Same properties as the init parameter of fetch + */ + +/** + * @typedef {object} sourceParams + * @property {string} protocol source's protocol (wmts, wms, wfs, file, tms, static) + * @property {string} url Base URL of the repository or of the file(s) to load + * @property {NetworkOptions} [networkOptions = { crossOrigin: 'anonymous' }] the base url to fetch data source + * @property {string} [projection] data's projection + * @property {Extent} [extent] data's extent + * @property {Attribution} [attribution] Attribution The intellectual property rights for the source + * @property {string} [format] data format + * + */ + +class Source { + /** + * Source are parameters to fetch source + * + * To extend {@link Source}, it is necessary to implement 2 functions: + * {@link Source#urlFromExtent} and {@link Source#extentInsideLimit} + * @param {sourceParams} source object to set source + * + */ + constructor(source) { + if (!source.protocol) { + throw new Error('New Source: protocol is required'); + } + + if (!source.url) { + throw new Error('New Source: url is required'); + } + + this.format = source.format; + this.protocol = source.protocol; + this.networkOptions = source.networkOptions || { crossOrigin: 'anonymous' }; + this.projection = source.projection; + this.attribution = source.attribution; + if (source.extent && !(source.extent instanceof Extent)) { + if (Array.isArray(source.extent)) { + this.extent = new Extent(this.projection, ...source.extent); + } else { + this.extent = new Extent(this.projection, source.extent); + } + } else { + this.extent = source.extent; + } + } + + handlingError(err) { + console.warn(`err ${this}`, err); + } + /** + * Generate url from extent. This url is link to fetch data inside the extent. + * + * @param {Extent} extent extent to convert in url + * @return {string} url from extent + */ + // eslint-disable-next-line + urlFromExtent(extent) { + throw new Error('In extented Source, you have to implement the method urlFromExtent!'); + } + + /** + * test if the extent is inside data's source + * + * @param {Extent} extent extent to test + * @return {boolean} return of test + */ + // eslint-disable-next-line + extentInsideLimit(extent) { + throw new Error('In extented Source, you have to implement the method extentInsideLimit!'); + } + + /** + * test if all extents are inside data's source + * + * @param {Array.} extents The extents to test + * @return {boolean} test result + */ + extentsInsideLimit(extents) { + for (const extent of extents) { + if (!this.extentInsideLimit(extent)) { + return false; + } + } + return true; + } +} + +export default Source; diff --git a/src/Source/StaticSource.js b/src/Source/StaticSource.js new file mode 100644 index 0000000000..bc2817088a --- /dev/null +++ b/src/Source/StaticSource.js @@ -0,0 +1,132 @@ +import flatbush from 'flatbush'; +import Source from './Source'; +import Fetcher from '../Provider/Fetcher'; +import Extent from '../Core/Geographic/Extent'; + +function _selectImagesFromSpatialIndex(index, images, extent) { + return index.search( + extent.west(), extent.south(), + extent.east(), extent.north()).map(i => images[i]); +} + +function buildUrl(layer, image) { + return layer.url.href.substr(0, layer.url.href.lastIndexOf('/') + 1) + + image; +} + +class StaticSource extends Source { + /** + * Images source to panorama layer {@link PanoramaLayer} + * + * @constructor + * @extends Source + * @param {sourceParams} source The source + * @param {string} source.extent It's extent of panoramic's images + */ + constructor(source) { + if (!source.extent) { + throw new Error('layer.extent is required'); + } + super(source); + + this.zoom = { min: 0, max: 0 }; + this.url = new URL(source.url, window.location); + this.whenReady = Fetcher.json(this.url.href).then((metadata) => { + this.images = []; + // eslint-disable-next-line guard-for-in + for (const image in metadata) { + const extent = new Extent(this.projection, ...metadata[image]); + this.images.push({ + image, + extent, + }); + } + if (!this.images.length) { + return; + } + this._spatialIndex = flatbush(this.images.length); + for (const image of this.images) { + this._spatialIndex.add( + image.extent.west(), + image.extent.south(), + image.extent.east(), + image.extent.north()); + } + this._spatialIndex.finish(); + }).then(() => { + if (!this.format) { + // fetch the first image to detect format + if (this.images.length) { + const url = buildUrl(this, this.images[0].image); + return fetch(url, this.networkOptions).then((response) => { + this.format = response.headers.get('Content-type'); + if (this.format === 'application/octet-stream') { + this.format = 'image/x-bil'; + } + if (!this.format) { + throw new Error(`${this.name}: could not detect layer format, please configure 'layer.format'.`); + } + }); + } + } + }); + } + + urlFromExtent(extent) { + const selection = this.getSourceExtents(extent); + return this.url.href.substr(0, this.url.href.lastIndexOf('/') + 1) + selection.image; + } + + handlingError(err, url) { + console.error(`Source static: ${this.url}, ${err.response.status} error while trying to fetch data. Url was ${url}.`, err); + } + + canTileTextureBeImproved(extent, texture) { + if (!this.images) { + return false; + } + const s = this.getSourceExtents(extent); + + if (!s) { + return false; + } + if (!texture.image) { + return true; + } + + const urlTexture = texture.image.currentSrc; + const urlSource = this.url.href.substr(0, this.url.href.lastIndexOf('/') + 1) + s.image; + const canBeImproved = urlSource != urlTexture; + return canBeImproved; + } + + getSourceExtents(extent) { + // select the smallest image entirely covering the tile + extent = extent.crs() === this.extent.crs() ? extent : extent.as(this.extent.crs()); + const candidates = + _selectImagesFromSpatialIndex( + this._spatialIndex, this.images, extent); + + let selection; + for (const entry of candidates) { + if (extent.isInside(entry.extent)) { + if (!selection) { + selection = entry; + } else { + const d = selection.extent.dimensions(); + const e = entry.extent.dimensions(); + if (e.x <= d.x && e.y <= d.y) { + selection = entry; + } + } + } + } + return selection; + } + + extentInsideLimit(extent) { + return extent.intersectsExtent(this.extent); + } +} + +export default StaticSource; diff --git a/src/Source/TMSSource.js b/src/Source/TMSSource.js new file mode 100644 index 0000000000..ca5d5d64eb --- /dev/null +++ b/src/Source/TMSSource.js @@ -0,0 +1,68 @@ +import Source from './Source'; +import URLBuilder from '../Provider/URLBuilder'; +import Extent from '../Core/Geographic/Extent'; + +class TMSSource extends Source { + /** + * Tiled images source + * @constructor + * @extends Source + * @param {sourceParams} source + * @param {string} [source.origin] origin row coordinate: 'top' or 'bottom' + * @param {Object} [source.zoom] + * @param {number} [source.zoom.min] layer's zoom minimum + * @param {number} [source.zoom.max] layer's zoom maximum + * @param {string} [source.tileMatrixSet='WGS84'] define tile matrix set of tms layer (ex: 'PM', 'WGS84') + * + * @example Add color layer with tms source + * const colorlayer = new ColorLayer('OPENSM', { + * source: { + * protocol: 'xyz', + * format: 'image/png', + * url: 'http://osm.io/styles/${z}/${x}/${y}.png', + * attribution: { + * name: 'OpenStreetMap', + * url: 'http://www.openstreetmap.org/', + * }, + * tileMatrixSet: 'PM', + * } + * }); + * // Add the layer + * view.addLayer(colorlayer); + */ + constructor(source) { + super(source); + if (!source.extent) { + // default to the full 3857 extent + this.extent = new Extent('EPSG:3857', + -20037508.342789244, 20037508.342789244, + -20037508.342789255, 20037508.342789244); + } + + this.zoom = source.zoom || { min: 0, max: 18 }; + + this.origin = source.origin || (source.protocol == 'xyz' ? 'top' : 'bottom'); + + this.format = this.format || 'image/png'; + this.url = source.url; + this.tileMatrixSet = source.tileMatrixSet || 'WGS84'; + } + + urlFromExtent(extent) { + return URLBuilder.xyz(extent, this); + } + + handlingError(err) { + console.warn(`err ${this.url}`, err); + } + + extentInsideLimit(extent) { + // This layer provides data starting at level = layer.source.zoom.min + // (the zoom.max property is used when building the url to make + // sure we don't use invalid levels) + // TODO: add extent limit + return extent.zoom >= this.zoom.min && extent.zoom <= this.zoom.max; + } +} + +export default TMSSource; diff --git a/src/Source/WFSSource.js b/src/Source/WFSSource.js new file mode 100644 index 0000000000..2d66f89704 --- /dev/null +++ b/src/Source/WFSSource.js @@ -0,0 +1,118 @@ +import Source from './Source'; +import URLBuilder from '../Provider/URLBuilder'; + +class WFSSource extends Source { + /** + * Features source + * @constructor + * @extends Source + * + * @param {sourceParams} source + * @param {string} source.typeName Name of the feature type to describe + * @param {string} source.projection crs of wfs + * @param {string} [source.version='2.0.2'] wfs protocol version + * @param {Object} [source.zoom] + * @param {number} [source.zoom.min] layer's zoom minimum + * @param {number} [source.zoom.max] layer's zoom maximum + * + * @example Add color layer with wfs source + * const colorlayer = new ColorLayer('color_build', { + * style: { + * fill: 'red', + * fillOpacity: 0.5, + * stroke: 'white', + * }, + * source: { + * url: 'http://wxs.fr/wfs', + * protocol: 'wfs', + * version: '2.0.0', + * typeName: 'BDTOPO_BDD_WLD_WGS84G:bati_remarquable', + * projection: 'EPSG:4326', + * extent: { + * west: 4.568, + * east: 5.18, + * south: 45.437, + * north: 46.03, + * }, + * format: 'application/json', + * } + * }); + * // Add the layer + * view.addLayer(colorlayer); + * + * @example Add geometry layer with wfs source + * const geometrylayer = new GeometryLayer('mesh_build', { + * update: itowns.FeatureProcessing.update, + * convert: itowns.Feature2Mesh.convert({ extrude: () => 50 }), + * source: { + * protocol: 'wfs', + * url: 'http://wxs.fr/wfs', + * version: '2.0.0', + * typeName: 'BDTOPO_BDD_WLD_WGS84G:bati_remarquable', + * projection: 'EPSG:4326', + * extent: { + * west: 4.568, + * east: 5.18, + * south: 45.437, + * north: 46.03, + * }, + * zoom: { min: 14, max: 14 }, + * format: 'application/json', + * } + * }); + * // Add the layer + * view.addLayer(geometrylayer); + */ + constructor(source) { + if (!source.typeName) { + throw new Error('source.typeName is required in wfs source.'); + } + + if (!source.projection) { + throw new Error('source.projection is required in wfs source'); + } + super(source); + + this.typeName = source.typeName; + this.format = this.format || 'application/json'; + this.version = source.version || '2.0.2'; + + this.url = `${source.url + }SERVICE=WFS&REQUEST=GetFeature&typeName=${this.typeName + }&VERSION=${this.version + }&SRSNAME=${this.projection + }&outputFormat=${this.format + }&BBOX=%bbox,${this.projection}`; + + this.zoom = source.zoom || { min: 0, max: 21 }; + } + + handlingError(err, url) { + if (err.response.status == 400) { + return err.response.text().then((text) => { + const getCapUrl = `${this.url}SERVICE=WFS&REQUEST=GetCapabilities&VERSION=${this.version}`; + const xml = new DOMParser().parseFromString(text, 'application/xml'); + const errorElem = xml.querySelector('Exception'); + const errorCode = errorElem.getAttribute('exceptionCode'); + const errorMessage = errorElem.querySelector('ExceptionText').textContent; + console.error(`Source ${this.typeName}: bad request when fetching data. Server says: "${errorCode}: ${errorMessage}". \nReviewing ${getCapUrl} may help.`, err); + throw err; + }); + } else { + console.error(`Source ${this.typeName}: ${err.response.status} error while trying to fetch WFS data. Url was ${url}.`, err); + throw err; + } + } + + urlFromExtent(extent) { + return URLBuilder.bbox(extent.as(this.projection), this); + } + + extentInsideLimit(extent) { + return (extent.zoom == undefined || (extent.zoom >= this.zoom.min && extent.zoom <= this.zoom.max)) + && this.extent.intersectsExtent(extent); + } +} + +export default WFSSource; + diff --git a/src/Source/WMSSource.js b/src/Source/WMSSource.js new file mode 100644 index 0000000000..1360e20b09 --- /dev/null +++ b/src/Source/WMSSource.js @@ -0,0 +1,100 @@ +import Source from './Source'; +import URLBuilder from '../Provider/URLBuilder'; + +class WMSSource extends Source { + /** + * Images source + * @constructor + * @extends Source + * @param {sourceParams} source + * @param {string} source.name name of layer wms + * @param {Extent} source.extent extent of wms source + * @param {string} [source.style=''] style of layer wms + * @param {number} [source.heightMapWidth=256] size texture in pixel + * @param {string} [source.version='1.3.0'] wms version + * @param {string} [source.axisOrder] wms axis order ('wsen' or 'swne') + * @param {boolean} [source.transparent=false] source return texture with transparence + * @param {Object} [source.zoom] + * @param {number} [source.zoom.min] layer's zoom minimum + * @param {number} [source.zoom.max] layer's zoom maximum + * + * @example Add color layer with wms source + * const colorlayer = new ColorLayer('Region', { + * source: { + * url: 'https://wxs.fr/wms', + * protocol: 'wms', + * version: '1.3.0', + * name: 'REGION.2016', + * style: '', + * projection: 'EPSG:3857', + * extent: { + * west: '-6880639.13557728', + * east: '6215707.87974825', + * south: '-2438399.00148845', + * north: '7637050.03850605', + * }, + * transparent: true, + * }, + * }); + * // Add the layer + * view.addLayer(colorlayer); + * + */ + constructor(source) { + if (!source.name) { + throw new Error('source.name is required.'); + } + + if (!source.extent) { + throw new Error('source.extent is required'); + } + + if (!source.projection) { + throw new Error('source.projection is required'); + } + super(source); + + this.name = source.name; + this.zoom = source.zoom || { min: 0, max: 21 }; + this.format = this.format || 'image/png'; + this.style = source.style || ''; + this.width = source.heightMapWidth || 256; + this.version = source.version || '1.3.0'; + this.transparent = source.transparent || false; + + if (!source.axisOrder) { + // 4326 (lat/long) axis order depends on the WMS version used + if (source.projection == 'EPSG:4326') { + // EPSG 4326 x = lat, long = y + // version 1.1.0 long/lat while version 1.3.0 mandates xy (so lat,long) + this.axisOrder = (this.version === '1.1.0' ? 'wsen' : 'swne'); + } else { + // xy,xy order + this.axisOrder = 'wsen'; + } + } + + const crsPropName = (this.version === '1.3.0') ? 'CRS' : 'SRS'; + + this.url = `${source.url}?SERVICE=WMS&REQUEST=GetMap&LAYERS=${ + this.name}&VERSION=${ + this.version}&STYLES=${ + this.style}&FORMAT=${ + this.format}&TRANSPARENT=${ + this.transparent}&BBOX=%bbox&${ + crsPropName}=${ + this.projection}&WIDTH=${this.width}&HEIGHT=${this.width}`; + } + + urlFromExtent(extent) { + return URLBuilder.bbox(extent, this); + } + + extentInsideLimit(extent) { + const localExtent = this.projection == extent.crs() ? extent : extent.as(this.projection); + return (extent.zoom == undefined || !(extent.zoom < this.zoom.min || extent.zoom > this.zoom.max)) && + this.extent.intersectsExtent(localExtent); + } +} + +export default WMSSource; diff --git a/src/Source/WMTSSource.js b/src/Source/WMTSSource.js new file mode 100644 index 0000000000..76e1c7a411 --- /dev/null +++ b/src/Source/WMTSSource.js @@ -0,0 +1,102 @@ +import Source from './Source'; +import URLBuilder from '../Provider/URLBuilder'; + +class WMTSSource extends Source { + /** + * Tiles images source + * @constructor + * @extends Source + * @param {sourceParams} source + * @param {string} source.name name of layer wmts + * @param {string} source.tileMatrixSet define tile matrix set of wmts layer (ex: 'PM', 'WGS84') + * @param {Array.} [source.tileMatrixSetLimits] The limits for the tile matrix set + * @param {number} source.tileMatrixSetLimits.minTileRow Minimum row for tiles at the level + * @param {number} source.tileMatrixSetLimits.maxTileRow Maximum row for tiles at the level + * @param {number} source.tileMatrixSetLimits.minTileCol Minimum col for tiles at the level + * @param {number} source.tileMatrixSetLimits.maxTileCol Maximum col for tiles at the level + * @param {Object} [source.zoom] + * @param {number} [source.zoom.min] layer's zoom minimum + * @param {number} [source.zoom.max] layer's zoom maximum + * + * @example Add color layer with wmts source + * const colorlayer = new ColorLayer('darkmap', { + * source: { + * protocol: 'wmts', + * name: 'DARK', + * tileMatrixSet: 'PM', + * url: 'http://server.geo/wmts', + * format: 'image/jpg', + * } + * }); + * // Add the layer + * view.addLayer(colorlayer); + * + */ + constructor(source) { + super(source); + + if (!source.name) { + throw new Error('New WMTSSource: name is required'); + } + + this.format = this.format || 'image/png'; + this.version = source.version || '1.0.0'; + this.tileMatrixSet = source.tileMatrixSet || 'WGS84'; + this.style = source.style || 'normal'; + this.name = source.name; + this.url = `${source.url}` + + `?LAYER=${this.name}` + + `&FORMAT=${this.format}` + + '&SERVICE=WMTS' + + `&VERSION=${this.version}` + + '&REQUEST=GetTile' + + `&STYLE=${this.style}` + + `&TILEMATRIXSET=${this.tileMatrixSet}` + + '&TILEMATRIX=%TILEMATRIX&TILEROW=%ROW&TILECOL=%COL'; + + this.zoom = source.zoom; + this.tileMatrixSetLimits = source.tileMatrixSetLimits; + + // If the projection is undefined, + // It is deduced from the tileMatrixSet, + // The projection is coherent with the projection + if (!this.projection) { + if (this.tileMatrixSet === 'WGS84' || this.tileMatrixSet === 'WGS84G') { + this.projection = 'EPSG:4326'; + } else { + this.projection = 'EPSG:3857'; + } + } + + if (!this.zoom) { + if (this.tileMatrixSetLimits) { + const arrayLimits = Object.keys(this.tileMatrixSetLimits); + const size = arrayLimits.length; + const maxZoom = Number(arrayLimits[size - 1]); + const minZoom = maxZoom - size + 1; + + this.zoom = { + min: minZoom, + max: maxZoom, + }; + } else { + this.zoom = { min: 2, max: 20 }; + } + } + } + + urlFromExtent(extent) { + return URLBuilder.xyz(extent, this); + } + + extentInsideLimit(extent) { + return extent.zoom >= this.zoom.min && extent.zoom <= this.zoom.max && + (this.tileMatrixSetLimits == undefined || + (extent.row >= this.tileMatrixSetLimits[extent.zoom].minTileRow && + extent.row <= this.tileMatrixSetLimits[extent.zoom].maxTileRow && + extent.col >= this.tileMatrixSetLimits[extent.zoom].minTileCol && + extent.col <= this.tileMatrixSetLimits[extent.zoom].maxTileCol)); + } +} + +export default WMTSSource; diff --git a/test/unit/layeredmaterial.js b/test/unit/layeredmaterial.js index aea7077074..44f60b6a82 100644 --- a/test/unit/layeredmaterial.js +++ b/test/unit/layeredmaterial.js @@ -17,6 +17,7 @@ describe('material state vs layer state', function () { setLayerOpacity: (idx, o) => { opacity = o; }, }, isDisplayed: () => true, + getCoordsForSource: () => 0, }; const layer = { id: 'test', diff --git a/test/unit/layeredmaterialnodeprocessing.js b/test/unit/layeredmaterialnodeprocessing.js index 03cd525b4c..9d7c3bdad6 100644 --- a/test/unit/layeredmaterialnodeprocessing.js +++ b/test/unit/layeredmaterialnodeprocessing.js @@ -28,22 +28,31 @@ describe('updateLayeredMaterialNodeImagery', function () { const layer = { id: 'foo', - protocol: 'dummy', - extent: new Extent('EPSG:4326', 0, 0, 0, 0), + source: { + protocol: 'dummy', + extent: new Extent('EPSG:4326', 0, 0, 0, 0), + }, }; beforeEach('reset state', function () { // clear commands array context.scheduler.commands = []; // reset default layer state - layer.tileInsideLimit = () => true; + + layer.updateStrategy = { + type: STRATEGY_MIN_NETWORK_TRAFFIC, + options: {}, + }; layer.visible = true; - layer.updateStrategy = STRATEGY_MIN_NETWORK_TRAFFIC; - layer.options = { + layer.source = { + protocol: 'dummy', + extentsInsideLimit: () => true, + extentInsideLimit: () => true, zoom: { min: 0, max: 10, }, + extent: { crs: () => 'EPSG:4326' }, }; }); @@ -88,6 +97,7 @@ describe('updateLayeredMaterialNodeImagery', function () { tile.material.indexOfColorLayer = () => 0; tile.material.isColorLayerDownscaled = () => true; tile.material.getColorLayerLevelById = () => 1; + tile.material.getLayerTextures = () => [{}]; // FIRST PASS: init Node From Parent and get out of the function // without any network fetch @@ -98,26 +108,31 @@ describe('updateLayeredMaterialNodeImagery', function () { assert.equal(context.scheduler.commands.length, 1); }); - it('tile should not request texture with level > layer.zoom.max', () => { + it('tile should not request texture with level > layer.source.zoom.max', () => { + const level = 15; + const countTexture = Math.pow(2, level); const tile = new TileMesh( layer, geom, new LayeredMaterial(), - new Extent('EPSG:4326', 0, 0, 0, 0), - 15); + new Extent('EPSG:4326', 0, 180 / countTexture, 0, 180 / countTexture), + level); tile.material.visible = true; tile.parent = { }; + layer.source.protocol = 'wmts'; + layer.source.tileMatrixSet = 'WGS84G'; // Emulate a situation where tile inherited a level 1 texture tile.material.indexOfColorLayer = () => 0; tile.material.isColorLayerDownscaled = () => true; tile.material.getColorLayerLevelById = () => 1; + tile.material.getLayerTextures = () => [{}]; // Since layer is using STRATEGY_MIN_NETWORK_TRAFFIC, we should emit - // a single command, requesting a texture at layer.options.zoom.max level + // a single command, requesting a texture at layer.source.zoom.max level updateLayeredMaterialNodeImagery(context, layer, tile); updateLayeredMaterialNodeImagery(context, layer, tile); assert.equal(context.scheduler.commands.length, 1); assert.equal( - context.scheduler.commands[0].targetLevel, - layer.options.zoom.max); + context.scheduler.commands[0].extentsSource[0].zoom, + layer.source.zoom.max); }); }); diff --git a/test/unit/vectortiles.js b/test/unit/vectortiles.js index 95d9d11481..80ed544e31 100644 --- a/test/unit/vectortiles.js +++ b/test/unit/vectortiles.js @@ -8,12 +8,8 @@ import Extent from '../../src/Core/Geographic/Extent'; const multipolygon = fs.readFileSync('test/data/pbf/multipolygon.pbf'); function parse(pbf) { - const coords = new Extent('TMS', 1, 1, 1); - const extent = new Extent( - 'EPSG:3857', - -20037508.342789244, 20037508.342789244, - -20037508.342789255, 20037508.342789244); - return VectorTileParser.parse(pbf, { coords, extent }); + pbf.coords = new Extent('TMS', 1, 1, 1); + return VectorTileParser.parse(pbf, { crsIn: 'EPSG:4326', crsOut: 'EPSG:3857' }); } describe('Vector tiles', function () { From 25c6021c1ac34e331a9a04e7c0c2cf54a9c482c2 Mon Sep 17 00:00:00 2001 From: gchoqueux Date: Thu, 23 Aug 2018 16:03:45 +0200 Subject: [PATCH 2/5] examples(core): add Source examples --- examples/globe_geojson_to3D.html | 80 ++++++++++++++++ examples/globe_wfs_color.html | 96 ++++++++++++++++++++ examples/index.html | 11 +++ examples/screenshots/globe_geojson_to3D.jpg | Bin 0 -> 23863 bytes examples/screenshots/globe_wfs_color.jpg | Bin 0 -> 39088 bytes 5 files changed, 187 insertions(+) create mode 100644 examples/globe_geojson_to3D.html create mode 100644 examples/globe_wfs_color.html create mode 100644 examples/screenshots/globe_geojson_to3D.jpg create mode 100644 examples/screenshots/globe_wfs_color.jpg diff --git a/examples/globe_geojson_to3D.html b/examples/globe_geojson_to3D.html new file mode 100644 index 0000000000..dc9b4daa1f --- /dev/null +++ b/examples/globe_geojson_to3D.html @@ -0,0 +1,80 @@ + + + Itowns - Globe + geoson to 3d + + + + + + + + +
+ +
+ + + + + + + + + diff --git a/examples/globe_wfs_color.html b/examples/globe_wfs_color.html new file mode 100644 index 0000000000..3c4738638b --- /dev/null +++ b/examples/globe_wfs_color.html @@ -0,0 +1,96 @@ + + + Itowns - Globe WFS color + + + + + + + + + +
+ + + + +
+

Information Batiment

+
    +
+
+ + + diff --git a/examples/index.html b/examples/index.html index 1576dfc1a7..8dd3af9425 100644 --- a/examples/index.html +++ b/examples/index.html @@ -147,6 +147,17 @@

synchronisation cameras with Ca + + +