helyOS State Management

Once you have built the Service Layer interacting with helyOS core, theoretically, you could start designing the user interfaces and visualization logics. However, for the multi-components consisted website, it’s very common that multi-components that share a same state or actions from different components may need to mutate the same state. Technically, state management solution, like Pinia or Vuex, could fix these kinds of problems and support more flexibility in large-scale production applications.

As state management was initially introduced in previous chapter, this chapter will help you complete more stores with Pinia related to helyOS. You might want to create your custom stores, you can also refer this chapter as store templates.

Leaflet Map

Leaflet map is used to display helyOS yards, shapes, tools and other map objects. When a map view object was initiated, it could be stored into a Pinia store. To implement this leaflet map store, you should define a new store:

./stores/leaflet-map-store.ts

import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useLeafletMapStore = defineStore('map', ()=>{
    const onClickCoords = ref(); // the coordinates of clicked point on map

    return{
        onClickCoords
    }

})

Then, define a leaflet map component:

./components/LeafletMap.vue

<template>
    <div id="mapContainer"></div>
    <div class="map-control">
        <button @click="goHome" class="mapBtn">Home</button>
        <br>
        <div class="coord-panel">{{ clickedPoint }}</div>
    </div>
</template>

<script setup lang="ts">
import { onMounted, ref, toRaw } from 'vue';
import "leaflet/dist/leaflet.css";
import L, { type LatLngExpression } from "leaflet";
import CheapRuler from "cheap-ruler";
import "leaflet.marker.slideto";
import 'leaflet-rotatedmarker'
import { useLeafletMapStore } from '@/stores/leaflet-map-store';
import { useToolStore } from '@/stores/tool-store';


const leafletMapStore = useLeafletMapStore(); // map store
const leafletMap = ref(); // map ref
const originLatLon = ref({ "lat": 51.0504, "lon": 13.7373 }); // yard 1
const zoomLevel = 17;
const toolMarkerLayer = new L.LayerGroup() // A layer group stores tool markers
const polygonLayer = new L.LayerGroup() // A layer group stores map objects

// initiate map
const initMap = (): any => {
    // map layers
    const osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 19,
        attribution: '© OpenStreetMap'
    });

    const esriImagery = L.tileLayer('http://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
        id: 'ESRI/Imagery',
        tileSize: 512,
        zoomOffset: -1,
        maxZoom: 19,
        attribution: '© OpenStreetMap | ESRI'
    });

    // base map
    const baseMaps = {
        "ESRI/Imagery": esriImagery,
        "OpenStreetMap": osm,
    };

    // map objects
    const overlays = {
        "helyOS Agents": toolMarkerLayer,
        "Map Objects": polygonLayer,
    }


    // initiate map
    leafletMap.value = L.map("mapContainer", {
        zoomControl: false,
        // zoomAnimation: false,
        center: [originLatLon.value.lat, originLatLon.value.lon],
        zoom: zoomLevel,
        layers: [osm]
    });

    // layer control
    L.control.layers(baseMaps, overlays).addTo(toRaw(leafletMap.value));


    onClickCoord();
};

// update map view
const updateMap = (originLat: number, originLon: number) => {
    leafletMap.value.remove(); // Destroys current map and clears all related event listeners
    initMap();
    originLatLon.value = { lat: originLat, lon: originLon };
    leafletMap.value.setView([originLatLon.value.lat, originLatLon.value.lon], zoomLevel);
}

// return two types coords of on click location
const clickedPoint = ref();
const onClickCoord = () => {
    // get MM coords
    leafletMap.value.on('click', (ev: any) => {
        let point = convertLatLngToMM(originLatLon.value.lat, originLatLon.value.lon, [[ev.latlng.lat, ev.latlng.lng]])
        console.log("Latlng: ", ev.latlng, "\nMM: ", point[0]);

        // coordinates panel
        clickedPoint.value = {
            LatLng: ev.latlng,
            MM: point[0]
        }

        // the destination of driving mission
        leafletMapStore.onClickCoords = ev.latlng

    })
}

// convert LatLng to MM
const convertLatLngToMM = (originLat: number, originLon: number, shapeLatLngPoints: number[][]) => {
    const ruler = new CheapRuler(originLat, 'meters'); // calculations around latitude
    const points = shapeLatLngPoints.map(point => {
        const distance = ruler.distance([originLon, originLat], [point[1], point[0]])
        const angle = ruler.bearing([originLon, originLat], [point[1], point[0]]) * Math.PI / 180;
        return [distance * 1000 * Math.sin(angle), distance * 1000 * Math.cos(angle)];
    });
    return points;
}


// go-home button
const goHome = () => {
    leafletMap.value.flyTo([originLatLon.value.lat, originLatLon.value.lon], zoomLevel);
};

// add GeoJson file
const geoJsonDisplay = (geojsonObj: any) => {
    // const geoJsonLayer = L.layerGroup(); // A layer group stores geojson objects
    // console.log(geojsonObj);
    polygonLayer.addLayer(L.geoJSON(geojsonObj)).addTo(toRaw(leafletMap.value));
};

// add polygon layer
const addPolygon = (polygon: LatLngExpression[] | any) => {
    // const polygonLayer = L.layerGroup() // A layer group stores polygon layers
    polygonLayer.addLayer(L.polygon(polygon)).addTo(toRaw(leafletMap.value));
};

// add tool marker layer
const toolStore = useToolStore(); // Tool store
const toolMarker = (tool: any) => {
    console.log("toolArray", tool);
    // const toolMarkerLayer = L.layerGroup() // A layer group stores tool markers

    if (tool.marker) { // marker existed
        if (tool.picture) {
            const markerIcon = L.icon({
                iconUrl: tool.picture,
                iconSize: [48, 48]
            });
            tool.marker.setIcon(markerIcon);
        }

        tool.marker.on('click', () => {
            toolStore.selectedTool = tool;
            toolStore.updateSelectedTool();
            // console.log(toolStore.selectedTool);
        });

        toolMarkerLayer.addLayer(tool.marker.bindPopup(tool.name));
        toolMarkerLayer.addTo(toRaw(leafletMap.value));

    } else { // marker not existed
        if (tool.picture) {
            const markerIcon = L.icon({
                iconUrl: tool.picture,
                iconSize: [48, 48]
            });
            const toolCoord = { lat: tool.y, lng: tool.x }
            tool.marker = L.marker(toolCoord).setIcon(markerIcon);
            tool.marker.setRotationOrigin('center center').setRotationAngle(tool.orientations[0]);
        }
        else {
            const toolCoord = { lat: tool.y, lng: tool.x }
            tool.marker = L.marker(toolCoord);
            tool.marker.setRotationOrigin('center center').setRotationAngle(tool.orientations[0]);
        }
        tool.marker.on('click', () => {
            toolStore.selectedTool = tool;
            toolStore.updateSelectedTool();
            // console.log(toolStore.selectedTool);
        });
        toolMarkerLayer.addLayer(tool.marker.bindPopup(tool.name));
        toolMarkerLayer.addTo(toRaw(leafletMap.value));
    }

};

// move marker to LatLng
const updateMarkerLatLng = (tool: any, toolPose: any) => {
    // console.log(tool, toolPose);
    const newLatLng = new L.LatLng(toolPose.lat, toolPose.lng);
    tool.marker.setRotationAngle(tool.orientations[0]).slideTo(newLatLng, { duration: 1000 });
};



// Mount
onMounted(() => {
    initMap();
});

// export default
defineExpose({
    updateMap, // update map view when switching yard
    addPolygon, // add polygon to the map
    geoJsonDisplay, // display geojson objects
    toolMarker, // initiate markers representing tools
    updateMarkerLatLng, // update markers location based on tools location
    leafletMap, // leaflet map
    clickedPoint // coords of clicked point
});

</script>

<style scoped>
#mapContainer {
    /* width: 1200px; */
    z-index: 0;
    height: 100%;
    display: flex;
}

.map-control {
    margin-bottom: 20px;
    position: relative;
    bottom: 50px;
    left: 10px;
    z-index: 10000;
}

.mapBtn {
    background-color: white;
    border: 1px solid darkgray;
    border-radius: 3px;
    margin-right: 5px;
}

.mapBtn:hover {
    background-color: lightgray;
}

.coord-panel {
    margin-top: 5px;
    background-color: white;
    display: inline-block;
    width: auto;
}
</style>

This leaflet map component contains all of methods interacting with map view, and store the map view object into leaflet map store.

Yard Store

Yard store contains two states selectedYard and yards, representing the id of selected yard by user and all of yard objects respectively. It also provides a method to get selected yard object.

./stores/yard-store.ts

import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { H_Yard } from 'helyosjs-sdk'

export const useYardStore = defineStore('yard', () => {
    // Initiate helyos yard store
    const selectedYard = ref("1") // yard id of current shown yard
    const yards = ref([] as H_Yard[]); // all of helyOS yard objects

    // get selected yard object
    const getCurrentYard = () => {
        return yards.value.filter((yards) => {
            return yards.id === selectedYard.value;
        })
    }

    return {
        yards,
        selectedYard,
        getCurrentYard,
    }

})

Tool Store

Yard store contains states about helyOS agents, and provides operations for tool objects between user interface and service layer.

./stores/tool-store.ts

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useYardStore } from './yard-store'
import type { H_Agents } from 'helyosjs-sdk'
import { patchTool, helyosService } from '@/services/helyos-service'

export const useToolStore = defineStore('tool', () => {
    // Initiate helyos tool store
    const tools = ref([] as H_Agents[]); // all of helyOS agent objects
    const ifSubscription = ref(0); // if 1, subscribe the pose updates of all tools, if 0, cancel the subscription
    const selectedTool = ref(); // selected tool
    const selectedToolInfo = ref(); // shown information of selected tool

    // get tools of selected yard from shape store
    const filterToolByYard = (yardId: string) => {
        console.log(tools.value);

        return tools.value.filter((tool) => {
            if(tool.yardId){
                return tool.yardId.toString() === yardId;
            }
        })
    }

    // patch all tools
    const patchToolIcon = (icon: any) => {
        tools.value.forEach((tool: H_Agents) => {
            // update icon of tool in tool store
            tool.picture = icon;

            // new tool
            const newTool = {
                id: tool.id,
                picture: icon,
            }

            // request patch tool operation
            patchTool(newTool);
        })
    }

    // convert coordinate from trucktrix format to latlng
    const convertToolToLatLng = (tool: H_Agents) => {
        const yardStore = useYardStore();
        const currentYard = yardStore.getCurrentYard();
        const toolLatLng = helyosService.convertMMtoLatLng(currentYard[0].lat, currentYard[0].lon, [[tool.x as number, tool.y as number]]);
        // console.log(toolLatLng);
        tool.x = toolLatLng[0][1]; // Lng as x
        tool.y = toolLatLng[0][0]; // lat as y
        tool.dataFormat = "LatLng-vehicle"
        return tool;
    }

    // update tools
    const updateToolMarkers = () => {
        tools.value.forEach((tool) => {
            const toolPose = {
                lat: tool.y,
                lon: tool.x
            }
            // tool.moveMarker(tool, toolPose);
        })
    }

    // update tool status information
    const updateSelectedTool = () => {
        // console.log(selectedTool.value);

        selectedToolInfo.value = {
            id: selectedTool.value.id,
            connectionStatus: selectedTool.value.connectionStatus,
            name: selectedTool.value.name,
            status: selectedTool.value.status,
            // sensors: selectedTool.value.sensors,
            lat: selectedTool.value.y,
            lon: selectedTool.value.x,
            orientation: selectedTool.value.orientation,
            yardId: selectedTool.value.yardId
        }
    }


    return {
        tools,
        ifSubscription,
        selectedToolInfo,
        selectedTool,
        filterToolByYard,
        patchToolIcon,
        updateSelectedTool,
        convertToolToLatLng,
        updateToolMarkers,
    }

})

Map Object Store

Map object store contains a mapObjects state to store all of helyOS map objects, and provides operations to upload map objects into helyOS database or delete map objects from helyOS database.

./stores/map-object-store.ts

import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { H_MapObject } from 'helyosjs-sdk'
import { pushNewMapObject, deleteMapObject } from '@/services/helyos-service'


export const useMapObjectStore = defineStore('map-object', () => {
    // Initiate helyos map objects store
    const mapObjects = ref([] as H_MapObject[]); // all of helyOS map object

    // get map objects of selected yard from map object store
    const filterMapObjectByYard = (yardId: string) => {
        return mapObjects.value.filter((mapObject) => {
            return mapObject.yardId === yardId;
        })
    }

    // push new MapObject
    const pushMapObject = async (mapObject: any) => {
        // push new MapObject into helyos database
        const newMapObject = await pushNewMapObject(mapObject);
        console.log(newMapObject);

        // push new MapObject into MapObject store
        if (newMapObject) {
            mapObjects.value.push(newMapObject);
            alert("Push successfully!");
        } else {
            alert("Push failed!")
        }
    }

    // delete all MapObjects of selected yard
    const deleteMapObjectsByYard = (yardId: string) => {
        // MapObjects to be deleted
        const deleteGroup = filterMapObjectByYard(yardId);
        console.log(deleteGroup);

        if (deleteGroup.length) {
            deleteGroup.forEach((mapObject) => {
                // delete MapObject from helyos database
                deleteMapObject(mapObject.id);

                // delete MapObject from MapObject store
                const index = mapObjects.value.indexOf(mapObject);
                if (index > -1) {
                    mapObjects.value.splice(index, 1);
                }
            })
            alert("Delete" + deleteGroup.length + " MapObject(s) successfully!")
        }
        else {
            alert("Nothing to be deleted!")
        }

    }

    return {
        mapObjects,
        filterMapObjectByYard,
        pushMapObject,
        deleteMapObjectsByYard,
    }

})

WorkProcess Store

WorkProcess store contains states including pre-defined Missions in helyOS Dashboard, WorkProcess objects, and selected mission(WorkProcessType).

./stores/work-process-store.ts

import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { H_WorkProcess, H_WorkProcessType } from 'helyosjs-sdk'
import { dispatchWorkProcess } from '@/services/helyos-service'

export const useWorkProcessStore = defineStore('work-process', ()=>{
    // Initiate helyos work process store
    const selectedMission = ref(); // selected work process type
    const workProcess = ref({}); // helyOS work process object
    const workProcessType = ref([] as H_WorkProcessType[]); // all work process types

    const dispatchMission = (toolId: number, yardId: any, requestMsg: any, settingMsg: any) => {
        workProcess.value = {
            toolIds: [toolId],
            yardId: yardId,
            workProcessTypeName: selectedMission.value,
            data: requestMsg,
            status: 'dispatched',
        }
        const missionLog = dispatchWorkProcess(workProcess.value as H_WorkProcess);
        console.log(missionLog);

    }

    return{
        selectedMission,
        workProcess,
        workProcessType,
        dispatchMission
    }

})