diff --git a/demo/js/index.js b/demo/js/index.js index 7b6fae3d..008c2e1d 100755 --- a/demo/js/index.js +++ b/demo/js/index.js @@ -27,539 +27,380 @@ import createInteractPlugin from '/plugins/interact/src/index.js' import createFramePlugin from '/plugins/beta/frame/src/index.js' const pointData = { - type: 'FeatureCollection', - features: [{ - type: 'Feature', - properties: { category: 'prehistoric', name: 'Prehistoric feature' }, - geometry: { coordinates: [-2.4558622, 54.5617135], type: 'Point' } - }, - { - type: 'Feature', - properties: { category: 'roman', name: 'Roman feature' }, - geometry: { coordinates: [-2.439823, 54.5525437], type: 'Point' } - }, - { - type: 'Feature', - properties: { category: 'medieval', name: 'Medieval feature' }, - geometry: { coordinates: [-2.4481939, 54.5575261], type: 'Point' } - }] + type: 'FeatureCollection', + features: [{ + type: 'Feature', + properties: { category:'prehistoric', name: 'Prehistoric feature' }, + geometry: { coordinates: [-2.4558622,54.5617135], type: 'Point' } + }, + { + type: 'Feature', + properties: { category: 'roman', name: 'Roman feature' }, + geometry: { coordinates: [-2.439823,54.5525437], type: 'Point' } + }, + { + type: 'Feature', + properties: { category:'medieval', name: 'Medieval feature' }, + geometry: { coordinates: [-2.4481939,54.5575261], type: 'Point'} + }] } -const adjustedPointData = { - type: 'FeatureCollection', - features: [{ - type: 'Feature', - properties: { category: 'prehistoric', name: 'Prehistoric feature' }, - geometry: { coordinates: [-2.4758622, 54.5617135], type: 'Point' } - }, - { - type: 'Feature', - properties: { category: 'roman', name: 'Roman feature' }, - geometry: { coordinates: [-2.449823, 54.5525437], type: 'Point' } - }, - { - type: 'Feature', - properties: { category: 'medieval', name: 'Medieval feature' }, - geometry: { coordinates: [-2.4981939, 54.5575261], type: 'Point' } - }] -} - - - - const interactPlugin = createInteractPlugin({ - layers: [{ - layerId: 'historic-monuments-prehistoric', - }, { - layerId: 'historic-monuments-roman', - }, { - layerId: 'historic-monuments-medieval', - }, { - layerId: 'land-covers-110', - // labelProperty: 'gid' - // idProperty: 'gid' - }, { - layerId: 'land-covers-130-131', - // labelProperty: 'gid' - // idProperty: 'gid' - }, { - layerId: 'land-covers-332', - // labelProperty: 'gid' - // idProperty: 'gid' - }, { - layerId: 'land-covers-379', - // labelProperty: 'gid' - // idProperty: 'gid' - }, { - layerId: 'land-covers-other', - // labelProperty: 'gid' - // idProperty: 'gid' - }, - // { - // layerId: 'hedge-control', - // idProperty: 'id' - // }, - // { - // layerId: 'OS/TopographicArea_1/Agricultural Land', - // idProperty: 'TOID' - // }, - { - layerId: 'fill-inactive.cold', - // idProperty: 'id' - }, { - layerId: 'stroke-inactive.cold', - // idProperty: 'id' - } - ], - debug: true, - interactionModes: ['selectMarker', 'selectFeature', 'placeMarker'], // e.g. ['selectMarker'], ['selectFeature'], ['placeMarker'], or combinations - multiSelect: true, - deselectOnClickOutside: true + layers: [{ + layerId: 'historic-monuments-prehistoric', + }, { + layerId: 'historic-monuments-roman', + }, { + layerId: 'historic-monuments-medieval', + }, { + layerId: 'land-covers-110', + // labelProperty: 'gid' + // idProperty: 'gid' + },{ + layerId: 'land-covers-130-131', + // labelProperty: 'gid' + // idProperty: 'gid' + },{ + layerId: 'land-covers-332', + // labelProperty: 'gid' + // idProperty: 'gid' + },{ + layerId: 'land-covers-379', + // labelProperty: 'gid' + // idProperty: 'gid' + },{ + layerId: 'land-covers-other', + // labelProperty: 'gid' + // idProperty: 'gid' + }, + // { + // layerId: 'hedge-control', + // idProperty: 'id' + // }, + // { + // layerId: 'OS/TopographicArea_1/Agricultural Land', + // idProperty: 'TOID' + // }, + { + layerId: 'fill-inactive.cold', + // idProperty: 'id' + },{ + layerId: 'stroke-inactive.cold', + // idProperty: 'id' + } + ], + debug: true, + interactionModes: ['selectMarker', 'selectFeature', 'placeMarker'], // e.g. ['selectMarker'], ['selectFeature'], ['placeMarker'], or combinations + multiSelect: true, + deselectOnClickOutside: true }) -const framePlugin = createFramePlugin({ aspectRatio: 1.5 }) - -const landCoversDataset = { - id: 'land-covers', - label: 'Land covers', - geojson: `${process.env.FARMING_API_URL}/api/collections/parcels/items?sbi=106325052`, // 106200212 - // filter: ["!",["in",["to-string",["id"]],["literal",["12"]]]], - // filter: [ - // 'all', - // ['!=', ['get', 'sbi'], '106223377'], - // ['==', ['get', 'is_dominant_land_cover'], true] - // ], - // tiles: ['https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges/{z}/{x}/{y}'], - // sourceLayer: 'field_parcels_filtered', - // featureLayer: '', - // idProperty: 'id', // Enables dynamic fetching + deduplication - // filter: ['get', ['propertyName', 'warning']], - query: {}, - transformRequest: transformDataRequest, // Builds URL with bbox - maxFeatures: 50000, // Optional: evict distant features when exceeded - minZoom: 10, - maxZoom: 24, - showInKey: true, - showInMenu: true, - style: { - symbolDescription: 'Land cover', - fillPattern: 'horizontal-hatch', - fillPatternForegroundColor: { outdoor: '#00897B', dark: '#ffffff' }, - fillPatternBackgroundColor: 'transparent' - }, - sublayers: [{ - id: '130-131', - label: 'Permanent grassland', - filter: ['in', ['get', 'dominant_land_cover'], ['literal', ['130', '131']]], // 'dominant_land_cover = "130"' - showInMenu: true, - style: { - stroke: { outdoor: '#00897B', dark: '#ffffff' }, - fillPattern: 'diagonal-cross-hatch', - fillPatternForegroundColor: { outdoor: '#00897B', dark: '#ffffff' }, - fillPatternBackgroundColor: 'transparent' - } - }, { - id: 'permanent-grassland-2', - label: 'Permanent grassland 2', - filter: ['in', ['get', 'dominant_land_cover'], ['literal', ['130', '131']]], // 'dominant_land_cover = "130"' - showInMenu: true, - style: { - stroke: { outdoor: '#00897B', dark: '#ffffff' }, - fillPattern: 'diagonal-cross-hatch', - fillPatternForegroundColor: { outdoor: '#00897B', dark: '#ffffff' }, - fillPatternBackgroundColor: 'transparent' - } - }, { - id: '332', - label: 'Woodland', - filter: ['==', ['get', 'dominant_land_cover'], '332'], - showInMenu: true, - style: { - stroke: { outdoor: '#2E7D32', dark: '#ffffff' }, - fillPattern: 'dot', - fillPatternForegroundColor: { outdoor: '#2E7D32', dark: '#ffffff' }, - fillPatternBackgroundColor: 'transparent' - } - }, { - id: '110', - label: 'Arable', - filter: ['==', ['get', 'dominant_land_cover'], '110'], - showInMenu: true, - style: { - stroke: { outdoor: '#6D4C41', dark: '#ffffff' }, - fillPattern: 'horizontal-hatch', - fillPatternForegroundColor: { outdoor: '#6D4C41', dark: '#ffffff' }, - fillPatternBackgroundColor: 'transparent' - } - }, { - id: '379', - label: 'Farmyards', - filter: ['==', ['get', 'dominant_land_cover'], '379'], - showInMenu: true, - style: { - stroke: { outdoor: '#6A1B9A', dark: '#ffffff' }, - fillPattern: 'forward-diagonal-hatch', - fillPatternForegroundColor: { outdoor: '#6A1B9A', dark: '#ffffff' }, - fillPatternBackgroundColor: 'transparent' - } - }, { - id: 'other', - label: 'Others', - filter: ['!', ['in', ['get', 'dominant_land_cover'], ['literal', ['110', '130', '131', '332', '379']]]], - showInMenu: true, - style: { - stroke: { outdoor: '#1565C0', dark: '#ffffff' }, - fill: 'rgba(0,0,255,0.1)', - fillPattern: 'vertical-hatch', - fillPatternForegroundColor: { outdoor: '#1565C0', dark: '#ffffff' }, - fillPatternBackgroundColor: 'transparent' - } - }] -} - -const existingFieldsDataset = { - id: 'existing-fields', - label: 'Existing fields', - groupLabel: 'Test group', - filter: ['all', ['==', ['get', 'sbi'], '106223377'], ['==', ['get', 'is_dominant_land_cover'], true]], - tiles: ['https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges/{z}/{x}/{y}'], - sourceLayer: 'field_parcels_filtered', - minZoom: 10, - maxZoom: 24, - showInKey: true, - showInMenu: true, - style: { - stroke: { outdoor: '#1565C0', dark: '#ffffff' }, - strokeWidth: 2, - fill: 'rgba(21,101,192,0.1)', - symbolDescription: { outdoor: 'blue outline', dark: 'white outline' } - } -} - -const historicMonumentsDataset = { - id: 'historic-monuments', - label: 'Historic monuments', - geojson: pointData, - minZoom: 10, - maxZoom: 24, - showInKey: true, - showInMenu: true, - // style: { - // symbol: 'square', - // symbolGraphic: 'M3 15H1V1h2v2h2V1h2v5h2V4h2v2h2V4h2v11H6V9H3v6z', // Historic monument - // // symbolAnchor: [0.1, 0.1], - // // symbolBackgroundColor: { outdoor: '#ca3535', dark: '#ffffff' }, - // // symbolForegroundColor: { outdoor: '#ffffff', dark: '#0b0c0c' } - // }, - sublayers: [{ - id: 'prehistoric', - label: 'Prehistoric', - filter: ['in', ['get', 'category'], 'prehistoric'], - showInMenu: true, - style: { - // symbolAnchor: [0.5, 0.5], - symbol: 'circle', - symbolGraphic: 'M3 15H1V1h2v2h2V1h2v5h2V4h2v2h2V4h2v11H6V9H3v6z', // Historic monument - symbolBackgroundColor: '#00897B', - } - }, { - id: 'roman', - label: 'Roman', - filter: ['in', ['get', 'category'], 'roman'], - showInMenu: true, - style: { - // symbolAnchor: [0.1, 0.1], - symbol: 'square', - symbolGraphic: 'M3 15H1V1h2v2h2V1h2v5h2V4h2v2h2V4h2v11H6V9H3v6z', // Historic monument - symbolBackgroundColor: '#ca3535', - } - }, { - id: 'medieval', - label: 'Medieval', - filter: ['in', ['get', 'category'], 'medieval'], - showInMenu: true, - style: { - // symbolAnchor: [0.9, 0.9], - symbol: 'square', - symbolGraphic: 'M3 15H1V1h2v2h2V1h2v5h2V4h2v2h2V4h2v11H6V9H3v6z', // Historic monument - symbolBackgroundColor: '#1565C0', - } - }] -} -const hedgeControlDataset = { - id: 'hedge-control', - label: 'Hedge control', - groupLabel: 'Test group', - tiles: ['https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges/{z}/{x}/{y}'], - sourceLayer: 'hedge_control', - minZoom: 10, - maxZoom: 24, - showInKey: true, - showInMenu: true, - visibility: 'hidden', - style: { - stroke: '#b58840', - fill: 'transparent', - strokeWidth: 4, - symbolDescription: { outdoor: 'blue outline' }, - keySymbolShape: 'line', - } -} +const framePlugin = createFramePlugin({ + aspectRatio: 1.5 +}) const datasetsPlugin = createDatasetsPlugin({ - layerAdapter: maplibreLayerAdapter, - // Example: Dynamic bbox-based fetching (uncomment to test) - datasets: [ - landCoversDataset, - existingFieldsDataset, - historicMonumentsDataset, - hedgeControlDataset - ] + layerAdapter: maplibreLayerAdapter, + // Example: Dynamic bbox-based fetching (uncomment to test) + datasets: [ + { + id: 'land-covers', + label: 'Land covers', + geojson: `${process.env.FARMING_API_URL}/api/collections/parcels/items?sbi=106325052`, // 106200212 + // filter: [ + // 'all', + // ['!=', ['get', 'sbi'], '106223377'], + // ['==', ['get', 'is_dominant_land_cover'], true] + // ], + // tiles: ['https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges/{z}/{x}/{y}'], + // sourceLayer: 'field_parcels_filtered', + // featureLayer: '', + // idProperty: 'id', // Enables dynamic fetching + deduplication + // filter: ['get', ['propertyName', 'warning']], + query: {}, + transformRequest: transformDataRequest, // Builds URL with bbox + maxFeatures: 50000, // Optional: evict distant features when exceeded + minZoom: 10, + maxZoom: 24, + showInKey: true, + showInMenu: true, + style: { + fillPattern: 'horizontal-hatch', + fillPatternForegroundColor: { outdoor: '#00897B', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + }, + sublayers: [{ + id: '130-131', + label: 'Permanent grassland', + filter: ['in', ['get', 'dominant_land_cover'], ['literal', ['130', '131']]], // 'dominant_land_cover = "130"' + showInMenu: true, + style: { + stroke: { outdoor: '#00897B', dark: '#ffffff' }, + fillPattern: 'diagonal-cross-hatch', + fillPatternForegroundColor: { outdoor: '#00897B', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + } + },{ + id: '332', + label: 'Woodland', + filter: ['==', ['get', 'dominant_land_cover'], '332'], + showInMenu: true, + style: { + stroke: { outdoor: '#2E7D32', dark: '#ffffff' }, + fillPattern: 'dot', + fillPatternForegroundColor: { outdoor: '#2E7D32', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + } + },{ + id: '110', + label: 'Arable', + filter: ['==', ['get', 'dominant_land_cover'], '110'], + showInMenu: true, + style: { + stroke: { outdoor: '#6D4C41', dark: '#ffffff' }, + fillPattern: 'horizontal-hatch', + fillPatternForegroundColor: { outdoor: '#6D4C41', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + } + },{ + id: '379', + label: 'Farmyards', + filter: ['==', ['get', 'dominant_land_cover'], '379'], + showInMenu: true, + style: { + stroke: { outdoor: '#6A1B9A', dark: '#ffffff' }, + fillPattern: 'forward-diagonal-hatch', + fillPatternForegroundColor: { outdoor: '#6A1B9A', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + } + },{ + id: 'other', + label: 'Others', + filter: ['!', ['in', ['get', 'dominant_land_cover'], ['literal', ['110', '130', '131', '332', '379']]]], + showInMenu: true, + style: { + stroke: { outdoor: '#1565C0', dark: '#ffffff' }, + fill: 'rgba(0,0,255,0.1)', + fillPattern: 'vertical-hatch', + fillPatternForegroundColor: { outdoor: '#1565C0', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + } + }] + }, + { + id: 'existing-fields', + label: 'Existing fields', + // groupLabel: 'Test group', + filter: ['all',['==', ['get', 'sbi'], '106223377'],['==', ['get', 'is_dominant_land_cover'], true]], + tiles: ['https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges/{z}/{x}/{y}'], + sourceLayer: 'field_parcels_filtered', + minZoom: 10, + maxZoom: 24, + showInKey: true, + showInMenu: true, + style: { + stroke: { outdoor: '#1565C0', dark: '#ffffff'}, + strokeWidth: 2, + fill: 'rgba(21,101,192,0.1)', + symbolDescription: { outdoor: 'blue outline', dark: 'white outline' } + } + },{ + id: 'historic-monuments', + label: 'Historic monuments', + geojson: pointData, + minZoom: 10, + maxZoom: 24, + showInKey: true, + showInMenu: true, + style: { + symbol: 'square', + symbolGraphic: 'M3 15H1V1h2v2h2V1h2v5h2V4h2v2h2V4h2v11H6V9H3v6z', // Historic monument + // symbolBackgroundColor: { outdoor: '#ca3535', dark: '#ffffff' }, + // symbolForegroundColor: { outdoor: '#ffffff', dark: '#0b0c0c' } + }, + sublayers: [{ + id: 'prehistoric', + label: 'Prehistoric', + filter: ['in', ['get', 'category'], 'prehistoric'], + showInMenu: true, + style: { + symbolBackgroundColor: '#00897B', + } + },{ + id: 'roman', + label: 'Roman', + filter: ['in', ['get', 'category'], 'roman'], + showInMenu: true, + style: { + symbolBackgroundColor: '#ca3535', + } + },{ + id: 'medieval', + label: 'Medieval', + filter: ['in', ['get', 'category'], 'medieval'], + showInMenu: true, + style: { + symbolBackgroundColor: '#1565C0', + } + }] + },{ + id: 'hedge-control', + label: 'Hedge control', + // groupLabel: 'Test group', + tiles: ['https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges/{z}/{x}/{y}'], + sourceLayer: 'hedge_control', + minZoom: 10, + maxZoom: 24, + showInKey: true, + showInMenu: true, + // visibility: 'hidden', + style: { + stroke: '#b58840', + fill: 'transparent', + strokeWidth: 4, + symbolDescription: { outdoor: 'blue outline' }, + keySymbolShape: 'line', + } + }] }) const interactiveMap = new InteractiveMap('map', { - behaviour: 'hybrid', - mapProvider: maplibreProvider(), - reverseGeocodeProvider: openNamesProvider({ - url: process.env.OS_NEAREST_URL, - // url: '/api/os-nearest-proxy?query={query}', - transformRequest: transformGeocodeRequest - // showMarker: true - }), - // maxMobileWidth: 700, - // minDesktopWidth: 960, - mapLabel: 'Map showing field parcels and land use', - // zoom: 14, - minZoom: 6, - maxZoom: 20, - autoColorScheme: true, - // center: [-2.938769, 54.893806], - bounds: [-2.450804, 54.5599279, -2.403804, 54.6199279], - containerHeight: '650px', - transformRequest: transformVtsRequest3857, - // readMapText: true, - // urlPosition: 'none', - // enableFullscreen: true, - // hasExitButton: true, - // markers: [{ - // id: 'location', - // coords: [-2.9592267, 54.9045977], - // color: { outdoor: '#ff0000', dark: '#00ff00' } - // }], - mapStyle: { - url: process.env.OZS_OUTDOOR_URL, - logo: '/assets/images/os-logo.svg', - logoAltText: 'Ordnance survey logo', - attribution: `Contains OS data ${String.fromCharCode(169)} Crown copyright and database rights ${(new Date()).getFullYear()}`, - backgroundColor: '#f5f5f0' - }, - // symbolDefaults: { - // symbol: 'circle', - // backgroundColor: { outdoor: '#1d70b8', dark: '#4c9ed9' }, - // haloColor: { outdoor: '#ffffff', dark: '#0b0c0c' }, - // selectedColor: { outdoor: '#ffdd00', dark: '#ffaa00' } - // }, - plugins: [ - datasetsPlugin, - mapStylesPlugin({ - mapStyles: vtsMapStyles3857 - }), - scaleBarPlugin({ - units: 'metric' - }), - searchPlugin({ - transformRequest: transformGeocodeRequest, - osNamesURL: process.env.OS_NAMES_URL, - customDatasets: [parcelSearch, gridRefSearchETRS89], - width: '300px', - showMarker: true - // expanded: true - }), - // useLocationPlugin(), - interactPlugin, - framePlugin - ] - // search + behaviour: 'hybrid', + mapProvider: maplibreProvider(), + reverseGeocodeProvider: openNamesProvider({ + url: process.env.OS_NEAREST_URL, + // url: '/api/os-nearest-proxy?query={query}', + transformRequest: transformGeocodeRequest + // showMarker: true + }), + // maxMobileWidth: 700, + // minDesktopWidth: 960, + mapLabel: 'Map showing field parcels and land use', + // zoom: 14, + minZoom: 6, + maxZoom: 20, + autoColorScheme: true, + // center: [-2.938769, 54.893806], + bounds: [-2.450804, 54.5599279, -2.403804, 54.6199279], + containerHeight: '650px', + transformRequest: transformVtsRequest3857, + // readMapText: true, + // urlPosition: 'none', + // enableFullscreen: true, + // hasExitButton: true, + // markers: [{ + // id: 'location', + // coords: [-2.9592267, 54.9045977], + // color: { outdoor: '#ff0000', dark: '#00ff00' } + // }], + mapStyle: { + url: process.env.OZS_OUTDOOR_URL, + logo: '/assets/images/os-logo.svg', + logoAltText: 'Ordnance survey logo', + attribution: `Contains OS data ${String.fromCharCode(169)} Crown copyright and database rights ${(new Date()).getFullYear()}`, + backgroundColor: '#f5f5f0' + }, + // symbolDefaults: { + // symbol: 'circle', + // backgroundColor: { outdoor: '#1d70b8', dark: '#4c9ed9' }, + // haloColor: { outdoor: '#ffffff', dark: '#0b0c0c' }, + // selectedColor: { outdoor: '#ffdd00', dark: '#ffaa00' } + // }, + plugins: [ + datasetsPlugin, + mapStylesPlugin({ + mapStyles: vtsMapStyles3857 + }), + scaleBarPlugin({ + units: 'metric' + }), + searchPlugin({ + transformRequest: transformGeocodeRequest, + osNamesURL: process.env.OS_NAMES_URL, + customDatasets: [parcelSearch, gridRefSearchETRS89], + width: '300px', + showMarker: true + // expanded: true + }), + // useLocationPlugin(), + interactPlugin, + framePlugin + ] + // search }) interactiveMap.on('app:ready', function (e) { - // console.log('app:ready') + // console.log('app:ready') }) interactiveMap.on('map:ready', function (e) { - // framePlugin.addFrame('test', { - // aspectRatio: 1 - // }) - interactPlugin.enable() - interactiveMap.addMarker('my-marker-1', [-2.4555608, 54.5655407], { label: 'My label', showLabel: true }) - interactiveMap.addMarker('my-marker-2', [-2.4511636, 54.5638338], { label: 'Another marker', symbol: 'square' }) + // framePlugin.addFrame('test', { + // aspectRatio: 1 + // }) + interactPlugin.enable() + interactiveMap.addMarker('my-marker-1', [-2.4555608,54.5655407], { label: 'My label', showLabel: true }) + interactiveMap.addMarker('my-marker-2', [-2.4511636,54.5638338], { label: 'Another marker', symbol: 'square' }) }) -// Datasets apiTests -const testVisibility = () => { - // Hide all landcovers - setTimeout(() => datasetsPlugin.setDatasetVisibility(false, { datasetId: 'land-covers' }), 2000) - // Specifically hide landcovers-130-131 - setTimeout(() => datasetsPlugin.setDatasetVisibility(false, { datasetId: 'land-covers', sublayerId: '130-131' }), 3000) - // Show landcovers - expect landcovers-130-131 to remain hidden - setTimeout(() => datasetsPlugin.setDatasetVisibility(true, { datasetId: 'land-covers' }), 4000) - // now reshow show landcovers-130-131 - setTimeout(() => datasetsPlugin.setDatasetVisibility(true, { datasetId: 'land-covers', sublayerId: '130-131' }), 5000) -} - -const testGlobalVisibility = () => { - setTimeout(() => datasetsPlugin.setDatasetVisibility(false), 1000) - setTimeout(() => datasetsPlugin.setDatasetVisibility(true), 5000) - setTimeout(() => datasetsPlugin.setDatasetVisibility(true, { datasetId: 'hedge-control' }), 500) - setTimeout(() => datasetsPlugin.setStyle({ stroke: { outdoor: '#0000ff' }, }, { datasetId: 'hedge-control' }), 2000) -} - -const testFeatureVisibility = () => { - // 29 and 16 - // setTimeout(() => datasetsPlugin.setFeatureVisibility(false, [12, 28, 19, 6], { datasetId: 'land-covers', idProperty: null }), 2000) - // setTimeout(() => datasetsPlugin.setFeatureVisibility(true, [12, 28], { datasetId: 'land-covers', idProperty: null }), 4000) - setTimeout(() => datasetsPlugin.setFeatureVisibility(false, [29], { datasetId: 'land-covers-130-131', idProperty: null }), 2000) - setTimeout(() => datasetsPlugin.setFeatureVisibility(false, [16], { datasetId: 'land-covers-permanent-grassland-2', idProperty: null }), 2000) -} - -const testOpacity = () => { - setTimeout(() => datasetsPlugin.setOpacity(0.5, { datasetId: 'land-covers', sublayerId: '130-131' }), 500) - setTimeout(() => datasetsPlugin.setOpacity(0, { datasetId: 'land-covers' }), 500) - setTimeout(() => datasetsPlugin.setOpacity(0.8, { datasetId: 'land-covers', sublayerId: '130-131' }), 1000) - setTimeout(() => datasetsPlugin.setOpacity(0.3, { datasetId: 'land-covers', sublayerId: '130-131' }), 1500) - setTimeout(() => datasetsPlugin.setOpacity(0.97, { datasetId: 'land-covers' }), 2000) - // setTimeout(() => datasetsPlugin.setOpacity(1, { datasetId: 'land-covers', sublayerId: '130-131' }), 4000) -} - -const testSetStyle = () => { - setTimeout(() => datasetsPlugin.setStyle({ - stroke: { outdoor: '#ff0000', dark: '#ffffff' }, - fillPattern: 'horizontal-hatch', - fillPatternForegroundColor: { outdoor: '#ff0000', dark: '#ffffff' }, - fillPatternBackgroundColor: 'transparent' - }, { datasetId: 'land-covers', sublayerId: '130-131' }), 1000) -} - -const testRemoveAndAddDataset = () => { - setTimeout(() => datasetsPlugin.removeDataset('historic-monuments'), 2000) - // setTimeout(() => datasetsPlugin.addDataset(landCoversDataset), 3000) - setTimeout(() => datasetsPlugin.addDataset({ ...historicMonumentsDataset, label: 'New historic monuments' }), 4000) -} - -const testGetters = () => { - setTimeout(() => { - console.log('Global Opacity', datasetsPlugin.getOpacity()) - console.log('Land Covers Opacity', datasetsPlugin.getOpacity({ datasetId: 'land-covers' })) - console.log('Land Covers-130-131 Opacity', datasetsPlugin.getOpacity({ datasetId: 'land-covers', sublayerId: '130-131' })) - console.log('Style without datasetId', datasetsPlugin.getStyle()) - console.log('Land Covers Opacity', datasetsPlugin.getStyle({ datasetId: 'land-covers' })) - console.log('Land Covers-130-131 Opacity', datasetsPlugin.getStyle({ datasetId: 'land-covers', sublayerId: '130-131' })) - }, 5000) -} - -const testInvalidApiCalls = () => { - setTimeout(() => { - datasetsPlugin.setDatasetVisibility(false, { datasetId: 'non-existent-dataset' }) // Should log an error about dataset not found - datasetsPlugin.setOpacity(0.5, { datasetId: 'non-existent-dataset' }) // Should log an error about dataset not found - datasetsPlugin.addDataset(landCoversDataset) // Adding a dataset with an existing id - should log an error and ignore - datasetsPlugin.removeDataset('historic-monuments-non-existent') // Should log an error about dataset not found - datasetsPlugin.setFeatureVisibility(false, [29], { datasetId: 'invalid-id' }) - datasetsPlugin.setFeatureVisibility(true, [29], { datasetId: 'also-invalid-id' }) - datasetsPlugin.setStyle({ fillPattern: 'horizontal-hatch' }, { datasetId: 'land-covers', sublayerId: 'invalid-sublayer' }) // Should log an error about dataset not found - }, 300) -} - -const testSetData = () => { - // Should cause the historic monuments to creep across the map as the coordinates are updated every 500ms - const newData = { ...pointData } - const features = [...newData.features] - const feature1 = features[0] - const feature2 = features[1] - const feature3 = features[2] - let counter = 0 - const increment = 0.001 - // Update coordinates of features to simulate change in data - const updateCoordinates = () => { - feature1.geometry.coordinates[0] += increment - feature1.geometry.coordinates[1] += increment - feature2.geometry.coordinates[0] -= increment - feature2.geometry.coordinates[1] -= increment - feature3.geometry.coordinates[0] += increment - feature3.geometry.coordinates[1] -= increment - counter++ - if (counter < 10) { - setTimeout(updateCoordinates, 500) - } - datasetsPlugin.setData(newData, { datasetId: 'historic-monuments' }) - } - setTimeout(updateCoordinates, 1000) -} - interactiveMap.on('datasets:ready', function () { - testGetters() - testInvalidApiCalls() - testFeatureVisibility() - testOpacity() - testSetStyle() - testVisibility() - testGlobalVisibility() - testRemoveAndAddDataset() - testSetData() + // setTimeout(() => datasetsPlugin.setFeatureVisibility(false, [55], { datasetId: 'land-covers', idProperty: null }), 2000) + // setTimeout(() => datasetsPlugin.setFeatureVisibility(true, [55], { datasetId: 'land-covers', idProperty: null }), 4000) + // setTimeout(() => datasetsPlugin.setStyle({ stroke: { outdoor: '#ff0000', dark: '#ffffff' }, fillPattern: 'horizontal-hatch', fillPatternForegroundColor: { outdoor: '#ff0000', dark: '#ffffff' } }, { datasetId: 'land-covers', sublayerId: '130' }), 2000) }) // Ref to the selected features let selectedFeatureIds = [] interactiveMap.on('interact:done', function (e) { - console.log('interact:done', e) + console.log('interact:done', e) }) interactiveMap.on('interact:cancel', function (e) { - console.log('interact:cancel', e) - interactPlugin.enable() + console.log('interact:cancel', e) + interactPlugin.enable() }) interactiveMap.on('interact:selectionchange', function (e) { - const drawLayers = ['stroke-inactive.cold', 'fill-inactive.cold'] - const singleFeature = e.selectedFeatures.length === 1 - const anyFeature = e.selectedFeatures.length > 0 - const isDrawFeature = singleFeature && drawLayers.includes(e.selectedFeatures[0].layerId) - const allDrawFeatures = anyFeature && e.selectedFeatures.every(function (f) { return drawLayers.includes(f.layerId) }) - selectedFeatureIds = e.selectedFeatures.map(function (f) { return f.featureId }) - interactiveMap.toggleButtonState('drawPolygon', 'disabled', !!singleFeature) - interactiveMap.toggleButtonState('drawLine', 'disabled', !!singleFeature) - interactiveMap.toggleButtonState('editFeature', 'disabled', !isDrawFeature) - interactiveMap.toggleButtonState('deleteFeature', 'disabled', !allDrawFeatures) + const drawLayers = ['stroke-inactive.cold', 'fill-inactive.cold'] + const singleFeature = e.selectedFeatures.length === 1 + const anyFeature = e.selectedFeatures.length > 0 + const isDrawFeature = singleFeature && drawLayers.includes(e.selectedFeatures[0].layerId) + const allDrawFeatures = anyFeature && e.selectedFeatures.every(function (f) { return drawLayers.includes(f.layerId) }) + selectedFeatureIds = e.selectedFeatures.map(function (f) { return f.featureId }) + interactiveMap.toggleButtonState('drawPolygon', 'disabled', !!singleFeature) + interactiveMap.toggleButtonState('drawLine', 'disabled', !!singleFeature) + interactiveMap.toggleButtonState('editFeature', 'disabled', !isDrawFeature) + interactiveMap.toggleButtonState('deleteFeature', 'disabled', !allDrawFeatures) }) interactiveMap.on('interact:markerchange', function (e) { - // console.log('interact:markerchange', e) + // console.log('interact:markerchange', e) }) // Update selected feature interactiveMap.on('search:match', function (e) { - if (e.type !== 'parcel') { - return - } - // Need to determine the layerId - // interactPlugin.selectFeature({ - // featureId: e.id, - // layerId: 'existing-fields' - // }) + if (e.type !== 'parcel') { + return + } + // Need to determine the layerId + // interactPlugin.selectFeature({ + // featureId: e.id, + // layerId: 'existing-fields' + // }) }) // Hide selected feature interactiveMap.on('search:clear', function (e) { - // console.log('Search clear') + // console.log('Search clear') }) // Frame events interactiveMap.on('frame:done', function (e) { - console.log('frame:done') - drawPlugin.addFeature(e) + console.log('frame:done') + drawPlugin.addFeature(e) }) interactiveMap.on('frame:cancel', function (e) { - console.log('frame:cancel') - console.log(e) + console.log('frame:cancel') + console.log(e) }) \ No newline at end of file diff --git a/demo/js/ml-datasets.js b/demo/js/ml-datasets.js new file mode 100644 index 00000000..f120c221 --- /dev/null +++ b/demo/js/ml-datasets.js @@ -0,0 +1,528 @@ +import InteractiveMap from '../../src/index.js' +import { vtsMapStyles3857 } from './mapStyles.js' +import { parcelSearch, gridRefSearchETRS89 } from './searchCustomDatasets.js' +import { transformGeocodeRequest, transformVtsRequest3857, transformDataRequest } from './auth.js' +// Providers +import maplibreProvider from '/providers/maplibre/src/index.js' +import openNamesProvider from '/providers/beta/open-names/src/index.js' +// Plugins +import mapStylesPlugin from '/plugins/beta/map-styles/src/index.js' +import createDatasetsPlugin from '/plugins/beta/datasets/src/index.js' +import { maplibreLayerAdapter } from '/plugins/beta/datasets/src/adapters/maplibre/index.js' +import scaleBarPlugin from '/plugins/beta/scale-bar/src/index.js' +import searchPlugin from '/plugins/search/src/index.js' +import createInteractPlugin from '/plugins/interact/src/index.js' +import createFramePlugin from '/plugins/beta/frame/src/index.js' + +const pointData = { + type: 'FeatureCollection', + features: [{ + type: 'Feature', + properties: { category: 'prehistoric', name: 'Prehistoric feature' }, + geometry: { coordinates: [-2.4558622, 54.5617135], type: 'Point' } + }, + { + type: 'Feature', + properties: { category: 'roman', name: 'Roman feature' }, + geometry: { coordinates: [-2.439823, 54.5525437], type: 'Point' } + }, + { + type: 'Feature', + properties: { category: 'medieval', name: 'Medieval feature' }, + geometry: { coordinates: [-2.4481939, 54.5575261], type: 'Point' } + }] +} + +const adjustedPointData = { + type: 'FeatureCollection', + features: [{ + type: 'Feature', + properties: { category: 'prehistoric', name: 'Prehistoric feature' }, + geometry: { coordinates: [-2.4758622, 54.5617135], type: 'Point' } + }, + { + type: 'Feature', + properties: { category: 'roman', name: 'Roman feature' }, + geometry: { coordinates: [-2.449823, 54.5525437], type: 'Point' } + }, + { + type: 'Feature', + properties: { category: 'medieval', name: 'Medieval feature' }, + geometry: { coordinates: [-2.4981939, 54.5575261], type: 'Point' } + }] +} + + + + +const interactPlugin = createInteractPlugin({ + layers: [{ + layerId: 'historic-monuments-prehistoric', + }, { + layerId: 'historic-monuments-roman', + }, { + layerId: 'historic-monuments-medieval', + }, { + layerId: 'land-covers-110', + // labelProperty: 'gid' + // idProperty: 'gid' + }, { + layerId: 'land-covers-130-131', + // labelProperty: 'gid' + // idProperty: 'gid' + }, { + layerId: 'land-covers-332', + // labelProperty: 'gid' + // idProperty: 'gid' + }, { + layerId: 'land-covers-379', + // labelProperty: 'gid' + // idProperty: 'gid' + }, { + layerId: 'land-covers-other', + // labelProperty: 'gid' + // idProperty: 'gid' + }, + // { + // layerId: 'hedge-control', + // idProperty: 'id' + // }, + // { + // layerId: 'OS/TopographicArea_1/Agricultural Land', + // idProperty: 'TOID' + // }, + { + layerId: 'fill-inactive.cold', + // idProperty: 'id' + }, { + layerId: 'stroke-inactive.cold', + // idProperty: 'id' + } + ], + debug: true, + interactionModes: ['selectMarker', 'selectFeature', 'placeMarker'], // e.g. ['selectMarker'], ['selectFeature'], ['placeMarker'], or combinations + multiSelect: true, + deselectOnClickOutside: true +}) + +const framePlugin = createFramePlugin({ aspectRatio: 1.5 }) + +const landCoversDataset = { + id: 'land-covers', + label: 'Land covers', + dynamicGeoJSON: { + idProperty: 'id', // required - the ID that identifies individual features + url: `${process.env.FARMING_API_URL}/api/collections/parcels/items?sbi=106325052`, // required + transformRequest: transformDataRequest, // Required + maxFeatures: 50000, // Optional: evict distant features when exceeded + }, + // filter: ["!",["in",["to-string",["id"]],["literal",["12"]]]], + // filter: [ + // 'all', + // ['!=', ['get', 'sbi'], '106223377'], + // ['==', ['get', 'is_dominant_land_cover'], true] + // ], + // tiles: ['https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges/{z}/{x}/{y}'], + // sourceLayer: 'field_parcels_filtered', + // featureLayer: '', + // idProperty: 'id', // Enables dynamic fetching + deduplication + // filter: ['get', ['propertyName', 'warning']], + query: {}, + minZoom: 10, + maxZoom: 24, + showInKey: true, + showInMenu: true, + style: { + symbolDescription: 'Land cover', + fillPattern: 'horizontal-hatch', + fillPatternForegroundColor: { outdoor: '#00897B', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + }, + sublayers: [{ + id: '130-131', + label: 'Permanent grassland', + filter: ['in', ['get', 'dominant_land_cover'], ['literal', ['130', '131']]], // 'dominant_land_cover = "130"' + showInMenu: true, + style: { + stroke: { outdoor: '#00897B', dark: '#ffffff' }, + fillPattern: 'diagonal-cross-hatch', + fillPatternForegroundColor: { outdoor: '#00897B', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + } + }, { + id: 'permanent-grassland-2', + label: 'Permanent grassland 2', + filter: ['in', ['get', 'dominant_land_cover'], ['literal', ['130', '131']]], // 'dominant_land_cover = "130"' + showInMenu: true, + style: { + stroke: { outdoor: '#00897B', dark: '#ffffff' }, + fillPattern: 'diagonal-cross-hatch', + fillPatternForegroundColor: { outdoor: '#00897B', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + } + }, { + id: '332', + label: 'Woodland', + filter: ['==', ['get', 'dominant_land_cover'], '332'], + showInMenu: true, + style: { + stroke: { outdoor: '#2E7D32', dark: '#ffffff' }, + fillPattern: 'dot', + fillPatternForegroundColor: { outdoor: '#2E7D32', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + } + }, { + id: '110', + label: 'Arable', + filter: ['==', ['get', 'dominant_land_cover'], '110'], + showInMenu: true, + style: { + stroke: { outdoor: '#6D4C41', dark: '#ffffff' }, + fillPattern: 'horizontal-hatch', + fillPatternForegroundColor: { outdoor: '#6D4C41', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + } + }, { + id: '379', + label: 'Farmyards', + filter: ['==', ['get', 'dominant_land_cover'], '379'], + showInMenu: true, + style: { + stroke: { outdoor: '#6A1B9A', dark: '#ffffff' }, + fillPattern: 'forward-diagonal-hatch', + fillPatternForegroundColor: { outdoor: '#6A1B9A', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + } + }, { + id: 'other', + label: 'Others', + filter: ['!', ['in', ['get', 'dominant_land_cover'], ['literal', ['110', '130', '131', '332', '379']]]], + showInMenu: true, + style: { + stroke: { outdoor: '#1565C0', dark: '#ffffff' }, + fill: 'rgba(0,0,255,0.1)', + fillPattern: 'vertical-hatch', + fillPatternForegroundColor: { outdoor: '#1565C0', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + } + }] +} + +const existingFieldsDataset = { + id: 'existing-fields', + label: 'Existing fields', + groupLabel: 'Test group', + filter: ['all', ['==', ['get', 'sbi'], '106223377'], ['==', ['get', 'is_dominant_land_cover'], true]], + tiles: ['https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges/{z}/{x}/{y}'], + sourceLayer: 'field_parcels_filtered', + minZoom: 10, + maxZoom: 24, + showInKey: true, + showInMenu: true, + style: { + stroke: { outdoor: '#1565C0', dark: '#ffffff' }, + strokeWidth: 2, + fill: 'rgba(21,101,192,0.1)', + symbolDescription: { outdoor: 'blue outline', dark: 'white outline' } + } +} + +const historicMonumentsDataset = { + id: 'historic-monuments', + label: 'Historic monuments', + geojson: pointData, + minZoom: 10, + maxZoom: 24, + showInKey: true, + showInMenu: true, + // style: { + // symbol: 'square', + // symbolGraphic: 'M3 15H1V1h2v2h2V1h2v5h2V4h2v2h2V4h2v11H6V9H3v6z', // Historic monument + // // symbolAnchor: [0.1, 0.1], + // // symbolBackgroundColor: { outdoor: '#ca3535', dark: '#ffffff' }, + // // symbolForegroundColor: { outdoor: '#ffffff', dark: '#0b0c0c' } + // }, + sublayers: [{ + id: 'prehistoric', + label: 'Prehistoric', + filter: ['in', ['get', 'category'], 'prehistoric'], + showInMenu: true, + style: { + // symbolAnchor: [0.5, 0.5], + symbol: 'circle', + symbolGraphic: 'M3 15H1V1h2v2h2V1h2v5h2V4h2v2h2V4h2v11H6V9H3v6z', // Historic monument + symbolBackgroundColor: '#00897B', + } + }, { + id: 'roman', + label: 'Roman', + filter: ['in', ['get', 'category'], 'roman'], + showInMenu: true, + style: { + // symbolAnchor: [0.1, 0.1], + symbol: 'square', + symbolGraphic: 'M3 15H1V1h2v2h2V1h2v5h2V4h2v2h2V4h2v11H6V9H3v6z', // Historic monument + symbolBackgroundColor: '#ca3535', + } + }, { + id: 'medieval', + label: 'Medieval', + filter: ['in', ['get', 'category'], 'medieval'], + showInMenu: true, + style: { + // symbolAnchor: [0.9, 0.9], + symbol: 'square', + symbolGraphic: 'M3 15H1V1h2v2h2V1h2v5h2V4h2v2h2V4h2v11H6V9H3v6z', // Historic monument + symbolBackgroundColor: '#1565C0', + } + }] +} +const hedgeControlDataset = { + id: 'hedge-control', + label: 'Hedge control', + groupLabel: 'Test group', + tiles: ['https://farming-tiles-702a60f45633.herokuapp.com/field_parcels_with_hedges/{z}/{x}/{y}'], + sourceLayer: 'hedge_control', + minZoom: 10, + maxZoom: 24, + showInKey: true, + showInMenu: true, + visibility: 'hidden', + style: { + stroke: '#b58840', + fill: 'transparent', + strokeWidth: 4, + symbolDescription: { outdoor: 'blue outline' }, + keySymbolShape: 'line', + } +} + +const datasetsPlugin = createDatasetsPlugin({ + layerAdapter: maplibreLayerAdapter, + // Example: Dynamic bbox-based fetching (uncomment to test) + datasets: [ + landCoversDataset, + existingFieldsDataset, + historicMonumentsDataset, + hedgeControlDataset + ] +}) + +const interactiveMap = new InteractiveMap('map', { + behaviour: 'hybrid', + mapProvider: maplibreProvider(), + reverseGeocodeProvider: openNamesProvider({ + url: process.env.OS_NEAREST_URL, + transformRequest: transformGeocodeRequest + }), + mapLabel: 'Map showing field parcels and land use', + minZoom: 6, + maxZoom: 20, + autoColorScheme: true, + bounds: [-2.450804, 54.5599279, -2.403804, 54.6199279], + containerHeight: '650px', + transformRequest: transformVtsRequest3857, + mapStyle: { + url: process.env.OZS_OUTDOOR_URL, + logo: '/assets/images/os-logo.svg', + logoAltText: 'Ordnance survey logo', + attribution: `Contains OS data ${String.fromCharCode(169)} Crown copyright and database rights ${(new Date()).getFullYear()}`, + backgroundColor: '#f5f5f0' + }, + plugins: [ + datasetsPlugin, + mapStylesPlugin({ + mapStyles: vtsMapStyles3857 + }), + scaleBarPlugin({ + units: 'metric' + }), + searchPlugin({ + transformRequest: transformGeocodeRequest, + osNamesURL: process.env.OS_NAMES_URL, + customDatasets: [parcelSearch, gridRefSearchETRS89], + width: '300px', + showMarker: true + // expanded: true + }), + interactPlugin, + framePlugin + ] +}) + +interactiveMap.on('app:ready', function (e) { + // console.log('app:ready') +}) + +interactiveMap.on('map:ready', function (e) { + interactPlugin.enable() +}) + +// Datasets apiTests +const testVisibility = () => { + // Hide all landcovers + setTimeout(() => datasetsPlugin.setDatasetVisibility(false, { datasetId: 'land-covers' }), 2000) + // Specifically hide landcovers-130-131 + setTimeout(() => datasetsPlugin.setDatasetVisibility(false, { datasetId: 'land-covers', sublayerId: '130-131' }), 3000) + // Show landcovers - expect landcovers-130-131 to remain hidden + setTimeout(() => datasetsPlugin.setDatasetVisibility(true, { datasetId: 'land-covers' }), 4000) + // now reshow show landcovers-130-131 + setTimeout(() => datasetsPlugin.setDatasetVisibility(true, { datasetId: 'land-covers', sublayerId: '130-131' }), 5000) +} + +const testGlobalVisibility = () => { + setTimeout(() => datasetsPlugin.setDatasetVisibility(false), 1000) + setTimeout(() => datasetsPlugin.setDatasetVisibility(true), 5000) + setTimeout(() => datasetsPlugin.setDatasetVisibility(true, { datasetId: 'hedge-control' }), 500) + setTimeout(() => datasetsPlugin.setStyle({ stroke: { outdoor: '#0000ff' }, }, { datasetId: 'hedge-control' }), 2000) +} + +const testFeatureVisibility = () => { + // 29 and 16 + // setTimeout(() => datasetsPlugin.setFeatureVisibility(false, [12, 28, 19, 6], { datasetId: 'land-covers', idProperty: null }), 2000) + // setTimeout(() => datasetsPlugin.setFeatureVisibility(true, [12, 28], { datasetId: 'land-covers', idProperty: null }), 4000) + setTimeout(() => datasetsPlugin.setFeatureVisibility(false, [29], { datasetId: 'land-covers-130-131', idProperty: null }), 2000) + setTimeout(() => datasetsPlugin.setFeatureVisibility(false, [16], { datasetId: 'land-covers-permanent-grassland-2', idProperty: null }), 2000) +} + +const testOpacity = () => { + setTimeout(() => datasetsPlugin.setOpacity(0.5, { datasetId: 'land-covers', sublayerId: '130-131' }), 500) + setTimeout(() => datasetsPlugin.setOpacity(0, { datasetId: 'land-covers' }), 500) + setTimeout(() => datasetsPlugin.setOpacity(0.8, { datasetId: 'land-covers', sublayerId: '130-131' }), 1000) + setTimeout(() => datasetsPlugin.setOpacity(0.3, { datasetId: 'land-covers', sublayerId: '130-131' }), 1500) + setTimeout(() => datasetsPlugin.setOpacity(0.97, { datasetId: 'land-covers' }), 2000) + // setTimeout(() => datasetsPlugin.setOpacity(1, { datasetId: 'land-covers', sublayerId: '130-131' }), 4000) +} + +const testSetStyle = () => { + setTimeout(() => datasetsPlugin.setStyle({ + stroke: { outdoor: '#ff0000', dark: '#ffffff' }, + fillPattern: 'horizontal-hatch', + fillPatternForegroundColor: { outdoor: '#ff0000', dark: '#ffffff' }, + fillPatternBackgroundColor: 'transparent' + }, { datasetId: 'land-covers', sublayerId: '130-131' }), 1000) +} + +const testRemoveAndAddDataset = () => { + setTimeout(() => datasetsPlugin.removeDataset('historic-monuments'), 2000) + // setTimeout(() => datasetsPlugin.addDataset(landCoversDataset), 3000) + setTimeout(() => datasetsPlugin.addDataset({ ...historicMonumentsDataset, label: 'New historic monuments' }), 4000) +} + +const testGetters = () => { + setTimeout(() => { + console.log('Global Opacity', datasetsPlugin.getOpacity()) + console.log('Land Covers Opacity', datasetsPlugin.getOpacity({ datasetId: 'land-covers' })) + console.log('Land Covers-130-131 Opacity', datasetsPlugin.getOpacity({ datasetId: 'land-covers', sublayerId: '130-131' })) + console.log('Style without datasetId', datasetsPlugin.getStyle()) + console.log('Land Covers Opacity', datasetsPlugin.getStyle({ datasetId: 'land-covers' })) + console.log('Land Covers-130-131 Opacity', datasetsPlugin.getStyle({ datasetId: 'land-covers', sublayerId: '130-131' })) + }, 5000) +} + +const testInvalidApiCalls = () => { + setTimeout(() => { + datasetsPlugin.setDatasetVisibility(false, { datasetId: 'non-existent-dataset' }) // Should log an error about dataset not found + datasetsPlugin.setOpacity(0.5, { datasetId: 'non-existent-dataset' }) // Should log an error about dataset not found + datasetsPlugin.addDataset(landCoversDataset) // Adding a dataset with an existing id - should log an error and ignore + datasetsPlugin.removeDataset('historic-monuments-non-existent') // Should log an error about dataset not found + datasetsPlugin.setFeatureVisibility(false, [29], { datasetId: 'invalid-id' }) + datasetsPlugin.setFeatureVisibility(true, [29], { datasetId: 'also-invalid-id' }) + datasetsPlugin.setStyle({ fillPattern: 'horizontal-hatch' }, { datasetId: 'land-covers', sublayerId: 'invalid-sublayer' }) // Should log an error about dataset not found + }, 300) +} + +const testSetData = () => { + // Should cause the historic monuments to creep across the map as the coordinates are updated every 500ms + const newData = { ...pointData } + const features = [...newData.features] + const feature1 = features[0] + const feature2 = features[1] + const feature3 = features[2] + let counter = 0 + const increment = 0.001 + // Update coordinates of features to simulate change in data + const updateCoordinates = () => { + feature1.geometry.coordinates[0] += increment + feature1.geometry.coordinates[1] += increment + feature2.geometry.coordinates[0] -= increment + feature2.geometry.coordinates[1] -= increment + feature3.geometry.coordinates[0] += increment + feature3.geometry.coordinates[1] -= increment + counter++ + if (counter < 10) { + setTimeout(updateCoordinates, 500) + } + datasetsPlugin.setData(newData, { datasetId: 'historic-monuments' }) + } + setTimeout(updateCoordinates, 1000) +} + +interactiveMap.on('datasets:ready', function () { + testGetters() + testInvalidApiCalls() + testFeatureVisibility() + testOpacity() + testSetStyle() + testVisibility() + testGlobalVisibility() + testRemoveAndAddDataset() + testSetData() +}) + +// Ref to the selected features +let selectedFeatureIds = [] + +interactiveMap.on('interact:done', function (e) { + console.log('interact:done', e) +}) + +interactiveMap.on('interact:cancel', function (e) { + console.log('interact:cancel', e) + interactPlugin.enable() +}) + +interactiveMap.on('interact:selectionchange', function (e) { + const drawLayers = ['stroke-inactive.cold', 'fill-inactive.cold'] + const singleFeature = e.selectedFeatures.length === 1 + const anyFeature = e.selectedFeatures.length > 0 + const isDrawFeature = singleFeature && drawLayers.includes(e.selectedFeatures[0].layerId) + const allDrawFeatures = anyFeature && e.selectedFeatures.every(function (f) { return drawLayers.includes(f.layerId) }) + selectedFeatureIds = e.selectedFeatures.map(function (f) { return f.featureId }) + interactiveMap.toggleButtonState('drawPolygon', 'disabled', !!singleFeature) + interactiveMap.toggleButtonState('drawLine', 'disabled', !!singleFeature) + interactiveMap.toggleButtonState('editFeature', 'disabled', !isDrawFeature) + interactiveMap.toggleButtonState('deleteFeature', 'disabled', !allDrawFeatures) +}) + +interactiveMap.on('interact:markerchange', function (e) { + // console.log('interact:markerchange', e) +}) + +// Update selected feature +interactiveMap.on('search:match', function (e) { + if (e.type !== 'parcel') { + return + } + // Need to determine the layerId + // interactPlugin.selectFeature({ + // featureId: e.id, + // layerId: 'existing-fields' + // }) +}) + +// Hide selected feature +interactiveMap.on('search:clear', function (e) { + // console.log('Search clear') +}) + +// Frame events +interactiveMap.on('frame:done', function (e) { + console.log('frame:done') + drawPlugin.addFeature(e) +}) + +interactiveMap.on('frame:cancel', function (e) { + console.log('frame:cancel') + console.log(e) +}) \ No newline at end of file diff --git a/demo/ml-datasets.html b/demo/ml-datasets.html new file mode 100644 index 00000000..f00ff9ef --- /dev/null +++ b/demo/ml-datasets.html @@ -0,0 +1,15 @@ + + + + + + React TypeScript Webpack App + + + + + +
+ + + \ No newline at end of file diff --git a/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.js b/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.js index 7570e76f..e9c45597 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.js +++ b/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.js @@ -3,10 +3,6 @@ import { MAX_TILE_ZOOM, hashString } from '../layerIds.js' import { anchorToMaplibre } from '../../../../../../../providers/maplibre/src/utils/symbolImages.js' export class MapLibreDataset extends Dataset { - get isDynamicSource () { - return typeof this.geojson === 'string' && !!this.idProperty && typeof this.transformRequest === 'function' - } - get visibility () { return this.visible ? 'visible' : 'none' } get fillLayerId () { @@ -72,12 +68,12 @@ export class MapLibreDataset extends Dataset { get sourceId () { if (this.isSublayer) { return this.parent.sourceId } + if (this.hasDynamicGeoJSON) { return this.dynamicGeoJSON.sourceId } if (this.tiles) { const tilesKey = Array.isArray(this.tiles) ? this.tiles.join(',') : this.tiles return `tiles-${hashString(tilesKey)}` } if (this.geojson) { - if (this.isDynamicSource) { return `geojson-dynamic-${this.id}` } if (typeof this.geojson === 'string') { return `geojson-${hashString(this.geojson)}` } return `geojson-${this.id}` } @@ -85,6 +81,9 @@ export class MapLibreDataset extends Dataset { } get source () { + if (this.hasDynamicGeoJSON) { + return this.dynamicGeoJSON.source + } if (this.tiles) { return { type: 'vector', @@ -94,8 +93,7 @@ export class MapLibreDataset extends Dataset { } } if (this.geojson) { - const data = this.isDynamicSource ? { type: 'FeatureCollection', features: [] } : this.geojson - return { type: 'geojson', data, generateId: true } + return { type: 'geojson', data: this.geojson, generateId: true } } return null } @@ -147,7 +145,7 @@ export class MapLibreDataset extends Dataset { } get _hiddenFeaturesIdExpression () { - return this.idProperty ? ['to-string', ['get', this.idProperty]] : ['to-string', ['id']] + return this.hasDynamicGeoJSON ? this.dynamicGeoJSON.hiddenFeaturesIdExpression : ['to-string', ['id']] } get _hiddenFeaturesFilter () { diff --git a/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.test.js b/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.test.js index 75300f02..aae9a381 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.test.js +++ b/plugins/beta/datasets/src/adapters/maplibre/datasets/mapLibreDataset.test.js @@ -18,13 +18,10 @@ describe('MapLibreDataset', () => { 'ds-bare': { id: 'ds-bare' }, 'ds-no-id-prop': { id: 'ds-no-id-prop', geojson: 'https://example.com/data', transformRequest: () => {} }, 'ds-no-transform': { id: 'ds-no-transform', geojson: 'https://example.com/data', idProperty: 'id' }, - // shared: dynamic geojson — used by isDynamicSource, sourceId, and source tests - 'ds-dynamic': { id: 'ds-dynamic', geojson: 'https://example.com/data', idProperty: 'gid', transformRequest: () => {} }, 'ds-static-url': { id: 'ds-static-url', geojson: 'https://example.com/static.geojson' }, // shared: tiles with no zoom — used by source minzoom and maxzoom fallback tests 'ds-tiles-no-zoom': { id: 'ds-tiles-no-zoom', tiles: ['https://example.com/{z}/{x}/{y}'] }, // getSymbolSource/getFillSource/getStrokeSource 'has filter' tests use historic-monuments-prehistoric and existing-fields from demo data - // _hiddenFeaturesIdExpression: ds-dynamic reused (has idProperty: 'gid') // _hiddenFeaturesFilter — ds-hf-123 also reused in filter describe 'ds-hf-123': { id: 'ds-hf-123', hiddenFeatures: [1, 2, 3] }, 'ds-hf-neg1': { id: 'ds-hf-neg1', hiddenFeatures: [-1, 5] }, @@ -97,24 +94,6 @@ describe('MapLibreDataset', () => { }) }) - describe('isDynamicSource', () => { - it('returns true when geojson is a string, idProperty is set, and transformRequest is a function', () => { - expect(datasetRegistry.getDataset('ds-dynamic').isDynamicSource).toBe(true) - }) - - it('returns false when geojson is an object', () => { - expect(datasetRegistry.getDataset('historic-monuments').isDynamicSource).toBe(false) - }) - - it('returns false when idProperty is missing', () => { - expect(datasetRegistry.getDataset('ds-no-id-prop').isDynamicSource).toBe(false) - }) - - it('returns false when transformRequest is not a function', () => { - expect(datasetRegistry.getDataset('ds-no-transform').isDynamicSource).toBe(false) - }) - }) - describe('visibility', () => { it('returns "visible" when visible is true', () => { expect(datasetRegistry.getDataset('existing-fields').visibility).toBe('visible') @@ -154,7 +133,7 @@ describe('MapLibreDataset', () => { expect(result).toEqual([{ layerIds: ['land-covers-130-131', 'land-covers-130-131-stroke'], filter: ['all', - ['!', ['in', ['to-string', ['id']], ['literal', ['42']]]], + ['!', ['in', ['to-string', ['get', 'id']], ['literal', ['42']]]], ['in', ['get', 'dominant_land_cover'], ['literal', ['130', '131']]]] }]) }) @@ -192,7 +171,7 @@ describe('MapLibreDataset', () => { }) it('returns geojson-dynamic-{id} for a dynamic geojson source', () => { - expect(datasetRegistry.getDataset('ds-dynamic').sourceId).toBe('geojson-dynamic-ds-dynamic') + expect(datasetRegistry.getDataset('land-covers').sourceId).toBe('geojson-dynamic-land-covers') }) it('returns geojson-{hash} for a static string geojson url', () => { @@ -229,7 +208,7 @@ describe('MapLibreDataset', () => { }) it('returns a geojson source with empty FeatureCollection for a dynamic geojson source', () => { - expect(datasetRegistry.getDataset('ds-dynamic').source).toEqual({ + expect(datasetRegistry.getDataset('land-covers').source).toEqual({ type: 'geojson', data: { type: 'FeatureCollection', features: [] }, generateId: true @@ -370,7 +349,7 @@ describe('MapLibreDataset', () => { describe('_hiddenFeaturesIdExpression', () => { it('uses get(idProperty) when idProperty is set', () => { - expect(datasetRegistry.getDataset('ds-dynamic')._hiddenFeaturesIdExpression).toEqual(['to-string', ['get', 'gid']]) + expect(datasetRegistry.getDataset('land-covers')._hiddenFeaturesIdExpression).toEqual(['to-string', ['get', 'id']]) }) it('uses the feature id when idProperty is not set', () => { diff --git a/plugins/beta/datasets/src/datasets.js b/plugins/beta/datasets/src/datasets.js index b14c2a50..ac3df9c8 100644 --- a/plugins/beta/datasets/src/datasets.js +++ b/plugins/beta/datasets/src/datasets.js @@ -27,12 +27,12 @@ export const createDatasets = ({ datasetRegistry.attach(mappedDatasets) adapter.init(mapStyle).then(() => { datasetRegistry.forEachDataset(registryDataset => { - if (!registryDataset.hasDynamicSource) { + if (!registryDataset.hasDynamicGeoJSON) { return } - + const { dynamicGeoJSON } = registryDataset const dynamicSource = createDynamicSource({ - dataset: registryDataset, + dynamicGeoJSON, map: mapProvider.map, onUpdate: (datasetId, geojson) => adapter.setData(datasetId, geojson) }) diff --git a/plugins/beta/datasets/src/fetch/createDynamicSource.js b/plugins/beta/datasets/src/fetch/createDynamicSource.js index 63d1417e..fd035796 100644 --- a/plugins/beta/datasets/src/fetch/createDynamicSource.js +++ b/plugins/beta/datasets/src/fetch/createDynamicSource.js @@ -8,14 +8,14 @@ const EVICTION_THRESHOLD = 1.2 // Trigger eviction at 120% of maxFeatures /** * Create a dynamic GeoJSON source that fetches data based on viewport * @param {Object} options - * @param {Object} options.dataset - Dataset configuration + * @param {Object} options.registryDataset - registryDataset instance * @param {Object} options.map - Map instance * @param {string} options.sourceId - Source ID for the map * @param {Function} options.onUpdate - Callback when source data should be updated * @returns {Object} { destroy, clear, refresh } */ -export const createDynamicSource = ({ dataset, map, onUpdate }) => { - const { geojson: baseUrl, idProperty, transformRequest, maxFeatures, minZoom = 0 } = dataset +export const createDynamicSource = ({ dynamicGeoJSON, map, onUpdate }) => { + const { url: baseUrl, idProperty, transformRequest, maxFeatures, minZoom = 0 } = dynamicGeoJSON // Feature cache: id → { feature, bbox, lastSeenAt } const features = new Map() @@ -112,7 +112,7 @@ export const createDynamicSource = ({ dataset, map, onUpdate }) => { currentController = new AbortController() try { - const context = { bbox: currentBbox, zoom, dataset } + const context = { bbox: currentBbox, zoom } const data = await fetchGeoJSON(baseUrl, context, transformRequest, currentController.signal) const now = Date.now() @@ -144,12 +144,12 @@ export const createDynamicSource = ({ dataset, map, onUpdate }) => { } // Update map source - onUpdate(dataset.id, toFeatureCollection()) + onUpdate(dynamicGeoJSON.id, toFeatureCollection()) } catch (error) { if (error.name === 'AbortError') { return } - console.error(`Failed to fetch dynamic GeoJSON for ${dataset.id}:`, error) + console.error(`Failed to fetch dynamic GeoJSON for ${dynamicGeoJSON.id}:`, error) } } @@ -184,7 +184,7 @@ export const createDynamicSource = ({ dataset, map, onUpdate }) => { clear () { features.clear() fetchedBbox = null - onUpdate(dataset.id, { type: 'FeatureCollection', features: [] }) + onUpdate(dynamicGeoJSON.id, { type: 'FeatureCollection', features: [] }) }, /** @@ -208,7 +208,7 @@ export const createDynamicSource = ({ dataset, map, onUpdate }) => { */ reapply () { if (features.size > 0) { - onUpdate(dataset.id, toFeatureCollection()) + onUpdate(dynamicGeoJSON.id, toFeatureCollection()) } } } diff --git a/plugins/beta/datasets/src/reducers/__data__/demoDatasets.js b/plugins/beta/datasets/src/reducers/__data__/demoDatasets.js index 1ff91184..0f27e637 100644 --- a/plugins/beta/datasets/src/reducers/__data__/demoDatasets.js +++ b/plugins/beta/datasets/src/reducers/__data__/demoDatasets.js @@ -19,7 +19,12 @@ export const datasets = [ { id: 'land-covers', label: 'Land covers', - geojson: `${process.env.FARMING_API_URL}/api/collections/parcels/items?sbi=106325052`, // 106200212 + dynamicGeoJSON: { + idProperty: 'id', // required - the ID that identifies individual features + url: `${process.env.FARMING_API_URL}/api/collections/parcels/items?sbi=106325052`, // required + transformRequest: (url) => url + 'TRANSFORMED', // Required + maxFeatures: 50000 // Optional: evict distant features when exceeded + }, hiddenFeatures: [42], query: {}, maxFeatures: 50000, // Optional: evict distant features when exceeded diff --git a/plugins/beta/datasets/src/registry/dataset.js b/plugins/beta/datasets/src/registry/dataset.js index 2f80b545..6cbe1109 100644 --- a/plugins/beta/datasets/src/registry/dataset.js +++ b/plugins/beta/datasets/src/registry/dataset.js @@ -1,6 +1,7 @@ import { datasetRegistry } from './datasetRegistry.js' import { hasCustomVisualStyle } from '../defaults.js' import { hasPattern } from '../../../../../src/utils/patternUtils.js' +import { DynamicGeoJson } from './dynamicGeoJson.js' export class Dataset { constructor (dataset) { @@ -16,7 +17,8 @@ export class Dataset { get tiles () { return this._datasetDefinition.tiles } get geojson () { return this._datasetDefinition.geojson } get idProperty () { return this._datasetDefinition.idProperty } - get transformRequest () { return this._datasetDefinition.transformRequest } + // TODO - handle transformRequest for non-dynamicGeoJSON as well (e.g. to add auth headers) --- IGNORE --- + // get transformRequest () { return this._datasetDefinition.transformRequest } get parentId () { return this._datasetDefinition.parentId } get minZoom () { return this._datasetDefinition.minZoom || this.parent?.minZoom } get maxZoom () { return this._datasetDefinition.maxZoom || this.parent?.maxZoom } @@ -26,8 +28,16 @@ export class Dataset { return this.style.opacity === undefined ? 1 : this.style.opacity } - get hasDynamicSource () { - return typeof this.geojson === 'string' && !!this.idProperty && typeof this.transformRequest === 'function' + get hasDynamicGeoJSON () { + return Boolean(this._datasetDefinition.dynamicGeoJSON) + } + + get dynamicGeoJSON () { + return new DynamicGeoJson({ + ...this._datasetDefinition.dynamicGeoJSON, + id: this.id, + minZoom: this.minZoom + }) } get hiddenFeatures () { return this._datasetDefinition.hiddenFeatures } diff --git a/plugins/beta/datasets/src/registry/dataset.test.js b/plugins/beta/datasets/src/registry/dataset.test.js index bccf5a46..c176af5e 100644 --- a/plugins/beta/datasets/src/registry/dataset.test.js +++ b/plugins/beta/datasets/src/registry/dataset.test.js @@ -197,7 +197,7 @@ describe('Dataset class', () => { }) }) - describe('tiles, geojson, idProperty, transformRequest, parentId', () => { + describe('tiles, geojson, idProperty, parentId', () => { it('returns tiles from the definition', () => { const tiles = ['https://example.com/{z}/{x}/{y}'] const dataset = new Dataset({ tiles }) @@ -215,12 +215,6 @@ describe('Dataset class', () => { expect(dataset.idProperty).toBe('gid') }) - it('returns transformRequest from the definition', () => { - const fn = () => {} - const dataset = new Dataset({ transformRequest: fn }) - expect(dataset.transformRequest).toBe(fn) - }) - it('returns parentId from the definition', () => { const dataset = new Dataset({ parentId: 'parent' }) expect(dataset.parentId).toBe('parent') @@ -270,25 +264,13 @@ describe('Dataset class', () => { }) }) - describe('hasDynamicSource', () => { - it('returns true when geojson is a string, idProperty is set, and transformRequest is a function', () => { - const dataset = new Dataset({ geojson: 'https://example.com/data', idProperty: 'id', transformRequest: () => {} }) - expect(dataset.hasDynamicSource).toBe(true) - }) - - it('returns false when geojson is an object (not a string)', () => { - const dataset = new Dataset({ geojson: { type: 'FeatureCollection', features: [] }, idProperty: 'id', transformRequest: () => {} }) - expect(dataset.hasDynamicSource).toBe(false) - }) - - it('returns false when idProperty is not set', () => { - const dataset = new Dataset({ geojson: 'https://example.com/data', transformRequest: () => {} }) - expect(dataset.hasDynamicSource).toBe(false) + describe('hasDynamicGeoJSON', () => { + it('returns true when definition has a dynamicGeoJSON object', () => { + expect(datasetRegistry.getDataset('land-covers').hasDynamicGeoJSON).toBe(true) }) - it('returns false when transformRequest is not a function', () => { - const dataset = new Dataset({ geojson: 'https://example.com/data', idProperty: 'id' }) - expect(dataset.hasDynamicSource).toBe(false) + it('returns false when definition does not have a dynamicGeoJSON object', () => { + expect(datasetRegistry.getDataset('historic-monuments').hasDynamicGeoJSON).toBe(false) }) }) diff --git a/plugins/beta/datasets/src/registry/dynamicGeoJson.js b/plugins/beta/datasets/src/registry/dynamicGeoJson.js new file mode 100644 index 00000000..f92c0fd4 --- /dev/null +++ b/plugins/beta/datasets/src/registry/dynamicGeoJson.js @@ -0,0 +1,17 @@ +export class DynamicGeoJson { + constructor (dynamicGeoJSONDefinition) { + this.id = dynamicGeoJSONDefinition.id + this.url = dynamicGeoJSONDefinition.url + this.transformRequest = dynamicGeoJSONDefinition.transformRequest + this.maxFeatures = dynamicGeoJSONDefinition.maxFeatures + this.minZoom = dynamicGeoJSONDefinition.minZoom + this.idProperty = dynamicGeoJSONDefinition.idProperty + this.hiddenFeaturesIdExpression = ['to-string', ['get', this.idProperty]] + this.sourceId = `geojson-dynamic-${this.id}` + this.source = { + type: 'geojson', + data: { type: 'FeatureCollection', features: [] }, + generateId: true + } + } +} diff --git a/webpack.dev.mjs b/webpack.dev.mjs index d097d993..3c9ab639 100755 --- a/webpack.dev.mjs +++ b/webpack.dev.mjs @@ -24,6 +24,7 @@ export default { planning: path.join(__dirname, 'demo/js/planning.js'), 'planning-ol': path.join(__dirname, 'demo/js/planning-ol.js'), gep: path.join(__dirname, 'demo/js/gep.js'), + 'ml-datasets': path.join(__dirname, 'demo/js/ml-datasets.js'), multimap: path.join(__dirname, 'demo/js/multimap.js') }, output: {