diff --git a/client/src/MapStore.ts b/client/src/MapStore.ts index 02fa62e..b1f4df4 100644 --- a/client/src/MapStore.ts +++ b/client/src/MapStore.ts @@ -7,6 +7,7 @@ import { ColorFilters, Context, Dataset, + DisplayConfiguration, LayerCollection, NetCDFData, NetCDFLayer, @@ -33,6 +34,10 @@ export default class MapStore { public static proModeButtonEnabled = ref(true); + public static displayConfiguration: Ref = ref( + { default_displayed_layers: [], enabled_ui: ['Collections', 'Datasets', 'Metadata'], default_tab: 'Scenarios' }, + ); + // Ability to toggle proMode so Staff users can see what other users see. public static proMode = computed(() => MapStore.userIsStaff.value && MapStore.proModeButtonEnabled.value); @@ -103,10 +108,57 @@ export default class MapStore { MapStore.mapLayersByDataset[datasetId] = await UVdatApi.getDatasetLayers(datasetId); } + public static async getDisplayConfiguration(initial = false) { + MapStore.displayConfiguration.value = await UVdatApi.getDisplayConfiguration(); + // Loading first time process default map layers + if (initial && MapStore.displayConfiguration.value.default_displayed_layers.length) { + const datasetIds = MapStore.displayConfiguration.value.default_displayed_layers.map((item) => item.dataset_id); + const datasetIdLayers = await UVdatApi.getDatasetsLayers(datasetIds); + const layerByDataset: Record = {}; + const toggleLayers: (VectorMapLayer | RasterMapLayer | NetCDFLayer)[] = []; + const enabledLayers = MapStore.displayConfiguration.value.default_displayed_layers; + datasetIdLayers.forEach((item) => { + if (item.dataset_id !== undefined) { + if (layerByDataset[item.dataset_id] === undefined) { + layerByDataset[item.dataset_id] = []; + } + layerByDataset[item.dataset_id].push(item); + } + enabledLayers.forEach((enabledLayer) => { + if (item.type === 'netcdf') { + if (enabledLayer.dataset_id === item.dataset_id) { + const netCDFLayers = ((item as NetCDFData).layers); + for (let i = 0; i < netCDFLayers.length; i += 1) { + const layer = netCDFLayers[i]; + if (layer.id === enabledLayer.id) { + toggleLayers.push(layer); + } + } + } + } else if ( + enabledLayer.type === item.type + && enabledLayer.id === item.id + && enabledLayer.dataset_id === item.dataset_id) { + toggleLayers.push(item); + } + }); + }); + Object.keys(layerByDataset).forEach((datasetIdKey) => { + const datasetId = parseInt(datasetIdKey, 10); + if (!Number.isNaN(datasetId)) { + MapStore.mapLayersByDataset[datasetId] = layerByDataset[datasetId]; + } + }); + // Now we enable these default layers + return toggleLayers; + } + return []; + } + public static mapLayerFeatureGraphs = computed(() => { const foundMapLayerFeatureGraphs: { name: string, id: number; graphs: VectorFeatureTableGraph[] }[] = []; MapStore.selectedVectorMapLayers.value.forEach((item) => { - if (item.default_style.mapLayerFeatureTableGraphs && item.default_style.mapLayerFeatureTableGraphs.length) { + if (item.default_style?.mapLayerFeatureTableGraphs && item.default_style.mapLayerFeatureTableGraphs.length) { foundMapLayerFeatureGraphs.push({ name: item.name, id: item.id, @@ -133,7 +185,7 @@ export default class MapStore { public static mapLayerVectorSearchable = computed(() => { const foundMapLayerSearchable: { name: string, id: number; searchSettings: SearchableVectorData }[] = []; MapStore.selectedVectorMapLayers.value.forEach((item) => { - if (item.default_style.searchableVectorFeatureData) { + if (item.default_style?.searchableVectorFeatureData) { foundMapLayerSearchable.push({ name: item.name, id: item.id, diff --git a/client/src/api/UVDATApi.ts b/client/src/api/UVDATApi.ts index b1797af..58143b7 100644 --- a/client/src/api/UVDATApi.ts +++ b/client/src/api/UVDATApi.ts @@ -3,11 +3,13 @@ import { ref } from 'vue'; import OauthClient from '@girder/oauth-client/dist/oauth-client'; import { AbstractMapLayer, + AbstractMapLayerListItem, Chart, Context, ContextWithIds, Dataset, DerivedRegion, + DisplayConfiguration, FeatureGraphData, FileItem, LayerCollection, @@ -213,7 +215,7 @@ export default class UVdatApi { return (await UVdatApi.apiClient.delete(`/files/${fileItemId}/`)).data; } - public static async getGlobalDatasets(filter: { unconnected: boolean }): Promise<(Dataset & { contextCount: number })[]> { + public static async getGlobalDatasets(filter?: { unconnected: boolean }): Promise<(Dataset & { contextCount: number })[]> { return (await UVdatApi.apiClient.get('datasets', { params: { ...filter } })).data.results; } @@ -450,6 +452,12 @@ export default class UVdatApi { return (await UVdatApi.apiClient.get(`/datasets/${datasetId}/map_layers`)).data; } + public static async getDatasetsLayers(datasetIds: number[]): Promise<(VectorMapLayer | RasterMapLayer | NetCDFData)[]> { + const params = new URLSearchParams(); + datasetIds.forEach((item) => params.append('datasetIds', item.toString())); + return (await UVdatApi.apiClient.get('/datasets/map_layers', { params })).data; + } + public static async getProcessingTasks(): Promise { return (await UVdatApi.apiClient.get('/processing-tasks')).data; } @@ -587,7 +595,32 @@ export default class UVdatApi { return (await UVdatApi.apiClient.get('/map-layers/', { params })).data; } + public static async getMapLayerAll(): Promise { + return (await UVdatApi.apiClient.get('/map-layers/all')).data; + } + public static async searchVectorFeatures(requestData: SearchableVectorDataRequest): Promise { return (await UVdatApi.apiClient.post('/map-layers/search-features/', requestData)).data; } + + public static async getDisplayConfiguration(): Promise { + const response = await UVdatApi.apiClient.get('display-configuration/'); + return response.data; + } + + // Fully update the display configuration (PUT /display_configuration/) + public static async updateDisplayConfiguration( + config: DisplayConfiguration, + ): Promise { + const response = await UVdatApi.apiClient.put('display-configuration/', config); + return response.data; + } + + // Partially update the display configuration (PATCH /display_configuration/) + public static async partialUpdateDisplayConfiguration( + config: Partial, + ): Promise { + const response = await UVdatApi.apiClient.patch('display-configuration/', config); + return response.data; + } } diff --git a/client/src/components/Admin/MapSelection.vue b/client/src/components/Admin/MapSelection.vue new file mode 100644 index 0000000..34e6be5 --- /dev/null +++ b/client/src/components/Admin/MapSelection.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/client/src/components/FeatureSelection/vectorFeatureGraphUtils.ts b/client/src/components/FeatureSelection/vectorFeatureGraphUtils.ts index 8a391ed..4194b20 100644 --- a/client/src/components/FeatureSelection/vectorFeatureGraphUtils.ts +++ b/client/src/components/FeatureSelection/vectorFeatureGraphUtils.ts @@ -230,7 +230,7 @@ const renderVectorFeatureGraph = ( g.append('path') .datum(graph.movingAverage) .attr('fill', 'none') - .attr('stroke', '#00FFFF') + .attr('stroke', '#FFFF00') .attr('stroke-width', 5) .attr('d', line) .attr('class', `moving-average moving-average-${key}`); diff --git a/client/src/components/Map.vue b/client/src/components/Map.vue index 880de32..128f887 100644 --- a/client/src/components/Map.vue +++ b/client/src/components/Map.vue @@ -66,6 +66,8 @@ export default defineComponent({ const initializeMap = () => { if (mapContainer.value) { + const center = MapStore.displayConfiguration.value.default_map_settings?.location.center || [-86.1794, 34.8019]; + const zoom = MapStore.displayConfiguration.value.default_map_settings?.location.zoom || 6; map.value = new maplibregl.Map({ container: mapContainer.value, style: { @@ -156,8 +158,8 @@ export default defineComponent({ sprite: 'https://maputnik.github.io/osm-liberty/sprites/osm-liberty', glyphs: 'https://orangemug.github.io/font-glyphs/glyphs/{fontstack}/{range}.pbf', }, - center: [-86.1794, 34.8019], // Coordinates for the relative center of the TVA - zoom: 6, // Initial zoom level + center, + zoom, }); if (map.value) { setInternalMap(map as Ref); @@ -237,7 +239,8 @@ export default defineComponent({ watch( MapStore.hoveredFeatures, () => { - if (map.value && (MapStore.mapLayerFeatureGraphsVisible.value || MapStore.activeSideBarCard.value === 'searchableVectors')) { + if (map.value + && (MapStore.mapLayerFeatureGraphsVisible.value || MapStore.activeSideBarCard.value === 'searchableVectors')) { updateSelected(map.value); } }, diff --git a/client/src/components/MapLegends/ColorKey.vue b/client/src/components/MapLegends/ColorKey.vue index 724448f..974f6d4 100644 --- a/client/src/components/MapLegends/ColorKey.vue +++ b/client/src/components/MapLegends/ColorKey.vue @@ -269,8 +269,11 @@ export default defineComponent({ // D3 allows color strings but says it requires numbers for type definitions .range(colors); // Recalculate percentage of width for gradient - const max = domain[domain.length - 1]; - const percent = domain.map((item) => (max === 0 ? 0 : item / max)); + const min = Math.min(...domain); + const max = Math.max(...domain); + const range = max - min; + + const percent = domain.map((item) => (range === 0 ? 0 : (item - min) / range)); // Append multiple color stops using data/enter step linearGradient.selectAll('stop').remove(); linearGradient diff --git a/client/src/components/TabularData/MapLayerTableGraph.vue b/client/src/components/TabularData/MapLayerTableGraph.vue index 24922a1..80b0268 100644 --- a/client/src/components/TabularData/MapLayerTableGraph.vue +++ b/client/src/components/TabularData/MapLayerTableGraph.vue @@ -35,9 +35,13 @@ export default defineComponent({ const maxMovingAverage = computed(() => { if (graphData.value) { - if (graphData.value?.graphs && graphData.value.graphs.all) { - const dataLength = graphData.value.graphs.all.data.length; - return Math.floor(dataLength / 4); + if (graphData.value?.graphs) { + const values = Object.values(graphData.value.graphs); + let max = -Infinity; + for (let i = 0; i < values.length; i += 1) { + max = Math.max(max, Math.floor(values[i].data.length / 4)); + } + return max; } } return 50; diff --git a/client/src/components/VectorFeatureSearch/Editor/VectorFeatureSearchEditor.vue b/client/src/components/VectorFeatureSearch/Editor/VectorFeatureSearchEditor.vue index 70dea2e..e401449 100644 --- a/client/src/components/VectorFeatureSearch/Editor/VectorFeatureSearchEditor.vue +++ b/client/src/components/VectorFeatureSearch/Editor/VectorFeatureSearchEditor.vue @@ -1,3 +1,4 @@ + + + + diff --git a/client/src/views/HomePage.vue b/client/src/views/HomePage.vue index 012af9e..a304e5f 100644 --- a/client/src/views/HomePage.vue +++ b/client/src/views/HomePage.vue @@ -1,6 +1,6 @@