Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"code": 130
}
],
"vuejs-accessibility/click-events-have-key-events": "off",
"vue/no-setup-props-destructure": 0,
"sort-imports": [
"error",
Expand Down
31 changes: 30 additions & 1 deletion client/src/MapStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import {
Ref, computed, reactive, ref,
} from 'vue';
import {
AnnotationTypes,
ClickedProps,
ColorFilters,
Context,
Dataset,
LayerCollection,
Expand All @@ -24,7 +26,7 @@ async function isVectorBaseMapAvailable(vectorMapUrl: string) {
type SideBarCard = 'indicators' | 'charts';

export default class MapStore {
public static osmBaseMap = ref<'none' | 'osm-raster' | 'osm-vector'>('osm-vector');
public static osmBaseMap = ref<'none' | 'osm-raster' | 'osm-vector'>('osm-raster');

public static userIsStaff = computed(() => !!UVdatApi.user?.is_staff);

Expand Down Expand Up @@ -254,4 +256,31 @@ export default class MapStore {
MapStore.sideBarCardSettings.value[key as SideBarCard].enabled = false;
});
};

public static vectorColorFilters: Ref<ColorFilters[]> = ref([]);

public static toggleColorFilter = (layerId: number, layerType: (AnnotationTypes | 'all'), key: string, value: string) => {
const foundIndex = MapStore.vectorColorFilters.value.findIndex(
(item) => item.layerId === layerId && layerType === item.layerType && key === item.key,
);
if (foundIndex === -1) {
MapStore.vectorColorFilters.value.push({
layerId,
layerType,
type: 'not in',
key,
values: new Set<string>([value]),
});
} else {
const found = MapStore.vectorColorFilters.value[foundIndex];
if (found.values.has(value)) {
found.values.delete(value);
if (found.values.size === 0) {
MapStore.vectorColorFilters.value.splice(foundIndex, 1);
}
} else {
found.values.add(value);
}
}
};
}
23 changes: 23 additions & 0 deletions client/src/api/UVDATApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,4 +538,27 @@ export default class UVdatApi {
});
return response.data;
}

public static async getMetadataFilters(): Promise<Record<string, string[]>> {
return (await UVdatApi.apiClient.get('/metadata-filters/get_filters/')).data;
}

public static async filterOnMetadata(
metdataFilters: Record<string, string[]>,
search?: string,
): Promise<{ id: number, type: AbstractMapLayer['type'], matches: string[], name: string }[]> {
return (await UVdatApi.apiClient.post('metadata-filters/filter_layers/', { filters: metdataFilters, search })).data;
}

public static async getMapLayerList(
layerIds: number[],
layerTypes : AbstractMapLayer['type'][],
): Promise<(VectorMapLayer | RasterMapLayer | NetCDFLayer)[]> {
const params = new URLSearchParams();

layerIds.forEach((id) => params.append('mapLayerIds', id.toString()));
layerTypes.forEach((id) => params.append('mapLayerTypes', id.toString()));

return (await UVdatApi.apiClient.get('/map-layers/', { params })).data;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const renderVectorFeatureGraph = (

y.domain(d3.extent(allYValues) as [number, number]);

const maxYValue = Math.max(...allYValues);
const maxYValue = Math.max(allYValues);
const maxYLabel = maxYValue.toFixed(2); // Format to 2 decimal places
const maxCharacters = maxYLabel.length;

Expand Down
35 changes: 33 additions & 2 deletions client/src/components/LayerTypeConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default defineComponent({
return enabled;
});

type LayerActionItems = 'enabled' | 'selectable' | 'hoverable' | 'opacity' | 'zoomMinMax' | 'selectColor' | 'defaultSize' | 'legend' | 'color' | 'text' | 'heatmapControls';
type LayerActionItems = 'enabled' | 'selectable' | 'hoverable' | 'opacity' | 'zoomMinMax' | 'selectColor' | 'defaultSize' | 'legend' | 'color' | 'text' | 'heatmapControls' | 'drawPoints';
const layerActionItemsMap: Record<LayerActionItems, AnnotationTypes[]> = {
enabled: ['line', 'fill', 'circle', 'fill-extrusion', 'text', 'heatmap'],
selectable: ['line', 'fill', 'circle', 'fill-extrusion'],
Expand All @@ -67,11 +67,12 @@ export default defineComponent({
color: ['line', 'fill', 'circle', 'fill-extrusion', 'text'],
text: ['text'],
heatmapControls: ['heatmap'],
drawPoints: ['line'],
};

const actionItemVisible = computed(() => {
const enabledItems = new Set<LayerActionItems>();
const itemList: LayerActionItems[] = ['enabled', 'selectable', 'hoverable', 'legend', 'opacity', 'zoomMinMax', 'selectColor', 'defaultSize', 'color', 'text', 'heatmapControls'];
const itemList: LayerActionItems[] = ['enabled', 'selectable', 'hoverable', 'legend', 'opacity', 'zoomMinMax', 'selectColor', 'defaultSize', 'color', 'text', 'heatmapControls', 'drawPoints'];
itemList.forEach((key) => {
if (layerActionItemsMap[key].includes(props.layerType)) {
enabledItems.add(key);
Expand Down Expand Up @@ -102,6 +103,10 @@ export default defineComponent({
if (field === 'legend') {
displayConfig.legend = val;
}
if (field === 'drawPoints') {
displayConfig.drawPoints = val;
}

if (field === 'opacity') {
if (val) {
displayConfig.opacity = 0.75;
Expand Down Expand Up @@ -683,6 +688,32 @@ export default defineComponent({
</v-tooltip>
</v-col>
</v-row>
<v-row
v-if="actionItemVisible.has('drawPoints')"
dense
align="center"
justify="center"
>
<v-col cols="2">
<v-tooltip text="Draw Points">
<template #activator="{ props }">
<v-icon
class="pl-3"
v-bind="props"
>
mdi-circle-outline
</v-icon>
</template>
</v-tooltip>
</v-col>
<v-col>
<v-icon @click="updateLayerTypeField('drawPoints', !valueDisplayCheckbox('drawPoints'))">
{{
valueDisplayCheckbox('drawPoints') ? 'mdi-checkbox-marked' : 'mdi-checkbox-blank-outline' }}
</v-icon>
<span class="pl-2">Draw Points</span>
</v-col>
</v-row>
<div v-if="actionItemVisible.has('heatmapControls')">
<heatmap-layer-controls :layer-id="layerId" :layer-type="layerType" />
</div>
Expand Down
7 changes: 1 addition & 6 deletions client/src/components/Map.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Protocol as PMTilesProtocol } from 'pmtiles';
import {
Ref, defineComponent, onMounted, ref, watch,
} from 'vue';
import MapStore, { VECTOR_PMTILES_URL } from '../MapStore';
import MapStore from '../MapStore';
import {
updateSelected,
} from '../map/mapVectorLayers';
Expand Down Expand Up @@ -81,10 +81,6 @@ export default defineComponent({
tileSize: 256,
attribution: '© OpenStreetMap contributors',
},
[OSM_VECTOR_ID]: {
type: 'vector',
url: `pmtiles://${VECTOR_PMTILES_URL}`,
},
'tva-region': {
type: 'geojson',
data: TVA_GEOJSON,
Expand Down Expand Up @@ -125,7 +121,6 @@ export default defineComponent({
minzoom: 0,
maxzoom: 19,
},
...VECTOR_LAYERS,
{
id: 'naip-imagery-tiles',
type: 'raster',
Expand Down
48 changes: 45 additions & 3 deletions client/src/components/MapLegends/ColorKey.vue
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<!-- eslint-disable vuejs-accessibility/click-events-have-key-events -->
<!-- eslint-disable vue/max-len -->
<script lang="ts">
import {
Expand All @@ -20,6 +21,7 @@ import {
} from '../../types'; // Import your defined types
import { createColorNumberPairs, formatNumPrecision, getLayerAvailableProperties } from '../../utils';
import MapStore from '../../MapStore';
import { updateLayerFilter } from '../../map/mapVectorLayers';

export default defineComponent({
name: 'ColorKey',
Expand Down Expand Up @@ -98,13 +100,20 @@ export default defineComponent({
} else if (
layerDisplayConfig.color.type === 'ColorCategoricalString'
) {
const found = MapStore.vectorColorFilters.value.find((item) => item.layerId === layer.id && item.layerType === 'all' && item.key === (layerDisplayConfig.color as ColorCategoricalString).attribute);
keyType.colors.push({
type: 'categorical',
attribute: (layerDisplayConfig.color as ColorCategoricalString).attribute,
pairs: Object.entries(
(layerDisplayConfig.color as ColorCategoricalString)
.colorPairs,
).map(([key, value]) => ({ value: key, color: value })),
).map(([key, value]) => {
let disabled;
if (found && found.values.has(key)) {
disabled = true;
}
return { value: key, color: value, disabled };
}),
});
} else if (
layerDisplayConfig.color.type === 'ColorCategoricalNumber'
Expand Down Expand Up @@ -312,6 +321,14 @@ export default defineComponent({
expandedPanels.value = opened;
setTimeout(() => drawGradients(), 100);
}, { immediate: true });

const toggleBoxColorFilter = (layerId: number, layerType: AnnotationTypes | 'all', key: string, value: string) => {
MapStore.toggleColorFilter(layerId, layerType, key, value);
const foundVectorLayer = MapStore.selectedVectorMapLayers.value.find((item) => item.id === layerId);
if (foundVectorLayer) {
updateLayerFilter(foundVectorLayer);
}
};
return {
capitalize,
processedLayers,
Expand All @@ -321,6 +338,7 @@ export default defineComponent({
drawDelay,
iconMapper,
expandedPanels,
toggleBoxColorFilter,
};
},
});
Expand Down Expand Up @@ -391,12 +409,20 @@ export default defineComponent({
justify="center"
>
<v-col>
<span>{{ pair.value }}: </span>
<span>{{ pair.value }}:</span>
</v-col>
<v-col cols="1">
<div
v-if="!pair.disabled"
class="color-icon"
:style="{ backgroundColor: pair.color }"
@click="toggleBoxColorFilter(layer.id, 'all', colorConfig.attribute, pair.value)"
/>
<div
v-else
class="color-icon"
:style="`border: 3px solid ${pair.color}`"
@click="toggleBoxColorFilter(layer.id, 'all', colorConfig.attribute, pair.value)"
/>
</v-col>
</v-row>
Expand Down Expand Up @@ -484,7 +510,11 @@ export default defineComponent({
:key="`${layer.id}_${keyType.type}_${index}`"
>
<v-expansion-panel-title style="font-size:0.75em">
<span v-if="!['netCDF', 'raster'].includes(keyType.type)"><v-icon v-if="iconMapper[keyType.type]" class="pr-2"> {{ iconMapper[keyType.type] }}</v-icon>{{ capitalize(keyType.type) }}</span>
<span v-if="!['netCDF', 'raster'].includes(keyType.type)">
<v-icon v-if="iconMapper[keyType.type]" class="pr-2">
{{ iconMapper[keyType.type] }}
</v-icon>{{ capitalize(keyType.type) }}
</span>
<span v-else-if="colorConfig.type === 'linearNetCDF'">{{ capitalize(colorConfig.value) }}</span>
<span v-if="['categorical', 'linear'].includes(colorConfig.type)">:
{{ attributeValues[layer.id][colorConfig.attribute].displayName }}
Expand All @@ -511,8 +541,16 @@ export default defineComponent({
</v-col>
<v-col cols="1">
<div
v-if="!pair.disabled"
class="color-icon"
:style="{ backgroundColor: pair.color }"
@click.stop="toggleBoxColorFilter(layer.id, 'all', colorConfig.attribute, pair.value)"
/>
<div
v-else
class="color-icon"
:style="`border: 3px solid ${pair.color}`"
@click.stop="toggleBoxColorFilter(layer.id, 'all', colorConfig.attribute, pair.value)"
/>
</v-col>
</v-row>
Expand Down Expand Up @@ -602,4 +640,8 @@ export default defineComponent({
height: 15px;
border: 1px solid gray;
}
.color-icon:hover {
cursor: pointer;
}

</style>
29 changes: 24 additions & 5 deletions client/src/components/MapLegends/ControlsKey.vue
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ export default defineComponent({
});
// Compute NetCDF Layer Keys
const stepIndexMap: Record<string, { length: number, currentIndex: number }> = {};
const resamplingMap: Record<string, 'linear' | 'nearest'> = {};
visibleNetCDFLayers.value.forEach((item) => {
const found = props.netcdfLayers.find((layer) => layer.id === item.netCDFLayer);
if (found) {
const { opacity } = item;
resamplingMap[`netcdf_${found.id}`] = item.resampling === 'nearest' ? 'nearest' : 'linear';
mapLayerOpacityMap[`netcdf_${found.id}`] = opacity !== undefined ? opacity : 1.0;
stepIndexMap[`netcdf_${found.id}`] = {
length: item.images.length,
Expand All @@ -87,7 +89,7 @@ export default defineComponent({
props.rasterLayers.forEach((layer) => {
mapLayerOpacityMap[`raster_${layer.id}`] = layer?.default_style?.opacity !== undefined ? layer.default_style.opacity : 1.0;
});
const order: { id: number, opacity: number, name: string, type: 'netcdf' | 'vector' | 'raster', length?: number, currentIndex?: number } [] = [];
const order: { id: number, opacity: number, name: string, type: 'netcdf' | 'vector' | 'raster', length?: number, currentIndex?: number, resampling?: 'linear' | 'nearest' } [] = [];
MapStore.selectedMapLayers.value.forEach((layer) => {
if (layer.type === 'netcdf') {
if (mapLayerOpacityMap[`netcdf_${layer.id}`] !== undefined) {
Expand All @@ -99,7 +101,7 @@ export default defineComponent({
currentIndex = data.currentIndex;
}
order.push({
id: layer.id, opacity: mapLayerOpacityMap[`netcdf_${layer.id}`], name: layer.name, type: layer.type, length, currentIndex,
id: layer.id, opacity: mapLayerOpacityMap[`netcdf_${layer.id}`], name: layer.name, type: layer.type, length, currentIndex, resampling: resamplingMap[`netcdf_${layer.id}`],
});
}
} else if (layer.type === 'raster') {
Expand Down Expand Up @@ -155,7 +157,7 @@ export default defineComponent({
});

const updateIndex = (layerId: number, currentIndex: number) => {
updateNetCDFLayer(layerId, currentIndex);
updateNetCDFLayer(layerId, { index: currentIndex });
};
const throttledUpdateNetCDFLayer = throttle(updateIndex, 50);

Expand All @@ -175,7 +177,7 @@ export default defineComponent({
const found = visibleNetCDFLayers.value.find((layer) => item.id === layer.netCDFLayer);
if (found) {
found.opacity = val;
updateNetCDFLayer(item.id, undefined, val);
updateNetCDFLayer(item.id, { opacity: val });
}
}
if (item.type === 'raster') {
Expand All @@ -190,12 +192,22 @@ export default defineComponent({
}
}
};

const toggleResampling = (id: number) => {
const found = visibleNetCDFLayers.value.find((layer) => id === layer.netCDFLayer);
if (found) {
const val = found.resampling === 'linear' ? 'nearest' : 'linear';
found.resampling = val;
updateNetCDFLayer(id, { resampling: val });
}
};
return {
processedLayers,
iconMapper,
updateOpacity,
throttledUpdateNetCDFLayer,
stepMapping,
toggleResampling,
};
},
});
Expand All @@ -211,7 +223,14 @@ export default defineComponent({
>
<v-card-text class="py-1 px-2">
<span class="py-1 px-2 d-flex align-center">
<v-icon size="16" class="mr-1" color="primary">
<v-tooltip v-if="item.type === 'netcdf'" text="Image Scaling (Nearest vs Linear)">
<template #activator="{ props }">
<v-icon size="16" v-bind="props" class="mr-1" color="primary" @click="toggleResampling(item.id)">
{{ item.resampling === 'nearest' ? ' mdi-view-grid' : 'mdi-grid' }}
</v-icon>
</template>
</v-tooltip>
<v-icon v-else size="16" class="mr-1" color="primary">
{{ iconMapper[item.type] }}
</v-icon>
<span class="text-sm">{{ item.name }}</span>
Expand Down
Loading