State Management - digitalcityscience/TOSCA-2 GitHub Wiki

State Management (Pinia)

This project uses Pinia for state management with Vue 3 Composition API. Pinia is a lightweight, modular, and TypeScript-compatible alternative to Vuex.

Important Note on Function Documentation

Each function within the store modules is documented using TSDoc comments. For detailed explanations of function parameters, return values, and usage, refer directly to the corresponding TypeScript files in the store/ directory.

For example:

  • Map-related state functions → Check store/map.ts
  • Filtering state functions → See store/filter.ts
  • Geoserver API state functions → Refer to store/geoserver.ts

For a complete breakdown of how each function works, refer to its respective file and TSDoc comments.


Store Structure

All Pinia stores are located in the src/store/ directory. Each store manages state for a specific part of the application.

📂 store
 ┣ 📜 buffer.ts        # Manages buffer-related state
 ┣ 📜 draw.ts          # Handles drawing tools state
 ┣ 📜 filter.ts        # Manages filtering state
 ┣ 📜 geoserver.ts     # Handles Geoserver interactions
 ┣ 📜 map.ts           # Manages map-related state
 ┣ 📜 participation.ts # Manages participation-related state

1. Map Store (map.ts)

Purpose:

  • Manages map state (zoom, center, active layers)
  • Handles interactions with MapLibre

Implementation:

import { defineStore } from "pinia";
import { ref } from "vue";
export const useMapStore = defineStore("map", () => {
    const map = ref<any>();
    const layersOnMap = ref<LayerObjectWithAttributes[]>([]);
        async function addMapLayer(
        params: LayerParams
    ): Promise<AddLayerObject | undefined> {
        const {
            sourceType,
            identifier,
            layerType,
            layerStyle,
            displayName,
            sourceIdentifier,
            showOnLayerList = true,
        } = params;
        if (isNullOrEmpty(map.value)) {
            throw new Error("There is no map to add layer");
        }
        if (identifier === "") {
            throw new Error("Identifier is required to add layer");
        }
        // Additional validation for geoserver source type
        if (sourceType === "geoserver" && !("geoserverLayerDetails" in params)) {
            throw new Error("Layer details required to add geoserver layers");
        }
        // Additional validation for geojson source type
        if (sourceType === "geojson" && !("geoJSONSrc" in params)) {
            throw new Error("GeoJSON data required to add GeoJSON layers");
        }
        const styling = generateStyling(layerType, layerStyle);
        const source = sourceIdentifier ?? identifier;

        const layerObject: CustomAddLayerObject = {
            id: identifier,
            source,
            sourceType,
            type: layerType,
            showOnLayerList,
            ...styling,
            // Conditional properties
            ...(sourceType === "geoserver" && params.sourceLayer != null
                ? { "source-layer": params.sourceLayer }
                : {}),
            ...(sourceType === "geojson" && params.isFilterLayer
                ? {
                    filterLayer: params.isFilterLayer,
                }
                : {}),
            ...(sourceType === "geojson" &&
      (params.isFilterLayer ||
        (params.isDrawnLayer !== undefined && params.isDrawnLayer))
                ? {
                    layerData: params.geoJSONSrc,
                }
                : {}),
            ...(displayName !== undefined && displayName !== ""
                ? { displayName }
                : {}),
        };
        // check if layer is raster type and add before vector layers
        let beforeId;
        let index;
        if (layerType === "raster") {
            const firstVectorLayer = layersOnMap.value.find((layer) => {
                return layer.type !== "raster";
            });
            const indexOfFirstVectorLayer = layersOnMap.value.findIndex((layer) => {
                return layer.type !== "raster";
            });
            if (indexOfFirstVectorLayer !== -1) {
                index = indexOfFirstVectorLayer;
            }
            if (firstVectorLayer !== undefined) {
                beforeId = firstVectorLayer.id;
            }
        }
        // add layer object to map
        map.value?.addLayer(layerObject as AddLayerObject, beforeId);
        if (map.value?.getLayer(identifier) === undefined) {
            throw new Error(`Couldn't add requested layer: ${identifier}`);
        }
        if (sourceType === "geoserver") {
            (layerObject as LayerObjectWithAttributes).details =
        params.geoserverLayerDetails;
            if (params.workspaceName !== undefined) {
                (layerObject as LayerObjectWithAttributes).workspaceName =
          params.workspaceName;
            }
        }
        add2MapLayerList(layerObject as LayerObjectWithAttributes, index);
        return map.value.getLayer(identifier) as AddLayerObject;
    }

    return {
        map,
        layersOnMap,
        addMapDataSource,
        deleteMapDataSource,
        addMapLayer,
        deleteMapLayer,
        removeFromMapLayerList,
        resetMapData,
        geometryConversion,
    };
});

2. Filtering Store (filter.ts)

Purpose:

  • Manages active attribute and geometry filters.
  • Uses Turf.js for spatial filtering.

Implementation:

import { defineStore } from "pinia";
import { ref } from "vue";
import booleanWithin from "@turf/boolean-within";
export const useFilterStore = defineStore("filter", () => {
    const appliedFiltersList = ref([]);
      async function populateLayerFilter(appliedFilters: AppliedFiltersListItem, attributeRelation: RelationTypes): Promise<any[]> {
    const expressionBlock: any[] = ["all"]
    if (appliedFilters.attributeFilters !== undefined){
      if (appliedFilters.attributeFilters.length > 0){
        const appliedFilterBlock: any[] = []
        if (attributeRelation === "AND"){
          appliedFilterBlock.push("all")
        } else {
          appliedFilterBlock.push("any")
        }
        appliedFilters.attributeFilters.forEach((filter)=>{
          if (filter.attribute.binding==="java.lang.String") {
            appliedFilterBlock.push([filter.operand, ["downcase", ["get", filter.attribute.name]], filter.value.toLowerCase()])
          } else {
            appliedFilterBlock.push([filter.operand, ["get", filter.attribute.name], parseFloat(filter.value)])
          }
        })
        if (appliedFilterBlock.length>0){
          expressionBlock.push(appliedFilterBlock)
        }
      }
    }
    if (appliedFilters.geometryFilters !== undefined){
      if (appliedFilters.geometryFilters.targetLayerSourceType === "fill"||appliedFilters.geometryFilters.targetLayerSourceType === "circle" || appliedFilters.geometryFilters.targetLayerSourceType === "line"){
        if (appliedFilters.geometryFilters.filterArray!.length>0){
          const geomFiltetExpression = ["in", ["get", appliedFilters.geometryFilters.identifier], ["literal", appliedFilters.geometryFilters.filterArray]]
          expressionBlock.push(geomFiltetExpression)
        }
      }
    }
    if (expressionBlock.length>1){
      return await Promise.resolve(expressionBlock)
    } else {
      return await Promise.resolve(expressionBlock)
    }
  }

    return {
    attributeList,
    integerFilters,
    stringFilters,
    filterNames,
    allowedBindings,
    allowedIDBindings,
    appliedFiltersList,
    addAttributeFilter,
    removeAttributeFilter,
    addGeometryFilter,
    removeGeometryFilter,
    createGeometryFilter,
    populateLayerFilter
  };
});

3. Geoserver Store (geoserver.ts)

Purpose:

  • Fetches metadata and feature attributes from GeoServer.
  • Retrieves bounding boxes and workspace layers.

Implementation:

import { defineStore } from "pinia";
import { ref } from "vue";
export const useGeoserverStore = defineStore("geoserver", () => {
  const layerList = ref<GeoserverLayerListItem[]>();
  const workspaceList = ref<WorkspaceListItem[]>();    
  async function getLayerList(
    workspaceName?: string
  ): Promise<GeoserverLayerListResponse> {
    let url = new URL(`${import.meta.env.VITE_GEOSERVER_REST_URL}/layers`);
    /* eslint-disable */
    if (workspaceName) {
      url = new URL(
        `${
          import.meta.env.VITE_GEOSERVER_REST_URL
        }/workspaces/${workspaceName}/layers`
      );
    }
    const response = await fetch(url, {
      method: "GET",
      redirect: "follow",
      headers: new Headers({
        "Content-Type": "application/json",
        Authorization: `Basic ${auth}`,
      }),
    });
    return await response.json();
  }

  return {
    pointData,
    layerList,
    workspaceList,
    getLayerList,
    getWorkspaceList,
    getLayerInformation,
    getLayerDetail,
    getGeoJSONLayerSource,
    getLayerStyling
  };
});

4. Participation Store (participation.ts)

Purpose:

  • Handles user participation in campaigns.
  • Communicates with the Django backend for participation data.

Implementation:

import { defineStore } from "pinia";
import { ref } from "vue";
export const useParticipationStore = defineStore("participation", () => {
    const campaigns = ref([]);
    
    function populateCampaignList(): void {
        getActiveCampaigns().then((campaigns) => {
            activeCampaigns.value = campaigns;
        }).catch((error) => {
            console.error(error);
        });
    }

    return {
        feedbackOnProgress,
        feedbackStep,
        sendFeedback,
        isLocationSelected,
        isCampaignRated,
        locationSelectionOnProgress,
        pointOfInterest,
        startCenterSelection,
        cancelCenterSelection,
        finishCenterSelection,
        selectedDrawnGeometry,
        addToSelectedDrawnGeometry,
        removeFromSelectedDrawnGeometry,
        createSelectedGeometryGeoJSON,
        createSelectedAreasTempLayer,
        updateSelectedAreasTempLayer,
        deleteSelectedAreasTempLayer,
        resetSelectedAreas,
        drawMode,
        getCampaignDetail,
        getActiveCampaigns,
        activeCampaigns,
        populateCampaignList
    };
});

Best Practices for Pinia

  • Use the Composition API with defineStore for better reactivity.
  • Keep state reactive using ref.
  • Define actions as functions inside the store.
  • Use Pinia DevTools for debugging state changes in Vue DevTools.

For more details, refer to the Vue 3 Pinia documentation.

⚠️ **GitHub.com Fallback** ⚠️