diff --git a/app_vue/README.md b/app_vue/README.md index 909b5ff4..ba7e4590 100644 --- a/app_vue/README.md +++ b/app_vue/README.md @@ -49,6 +49,9 @@ bin/vueLocalDeploy.sh ### Vue3 Leaflet - Map Library - this library allows us to generate a map and place relevant markers +### Google Maps Routes API +- this is used to calculate the optimal order of bins to visit +- note that if it returns empty object {} it is likely that your request is passing incorrect lat/long or location data ## Folder structure diff --git a/app_vue/src/assets/images/alert-bin.png b/app_vue/src/assets/images/binMarkerIcons/alert-bin.png similarity index 100% rename from app_vue/src/assets/images/alert-bin.png rename to app_vue/src/assets/images/binMarkerIcons/alert-bin.png diff --git a/app_vue/src/assets/images/battery-bin.png b/app_vue/src/assets/images/binMarkerIcons/battery-bin.png similarity index 100% rename from app_vue/src/assets/images/battery-bin.png rename to app_vue/src/assets/images/binMarkerIcons/battery-bin.png diff --git a/app_vue/src/assets/images/empty-bin.png b/app_vue/src/assets/images/binMarkerIcons/empty-bin.png similarity index 100% rename from app_vue/src/assets/images/empty-bin.png rename to app_vue/src/assets/images/binMarkerIcons/empty-bin.png diff --git a/app_vue/src/assets/images/fire-bin.png b/app_vue/src/assets/images/binMarkerIcons/fire-bin.png similarity index 100% rename from app_vue/src/assets/images/fire-bin.png rename to app_vue/src/assets/images/binMarkerIcons/fire-bin.png diff --git a/app_vue/src/assets/images/full-bin.png b/app_vue/src/assets/images/binMarkerIcons/full-bin.png similarity index 100% rename from app_vue/src/assets/images/full-bin.png rename to app_vue/src/assets/images/binMarkerIcons/full-bin.png diff --git a/app_vue/src/assets/images/half-full-bin.png b/app_vue/src/assets/images/binMarkerIcons/half-full-bin.png similarity index 100% rename from app_vue/src/assets/images/half-full-bin.png rename to app_vue/src/assets/images/binMarkerIcons/half-full-bin.png diff --git a/app_vue/src/assets/images/feather-disc.svg b/app_vue/src/assets/images/feather-disc.svg new file mode 100644 index 00000000..e20eb36a --- /dev/null +++ b/app_vue/src/assets/images/feather-disc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app_vue/src/assets/images/feather-map-pin.svg b/app_vue/src/assets/images/feather-map-pin.svg new file mode 100644 index 00000000..083acda2 --- /dev/null +++ b/app_vue/src/assets/images/feather-map-pin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app_vue/src/assets/images/generic-material.svg b/app_vue/src/assets/images/generic-material.svg deleted file mode 100644 index 17a8088d..00000000 --- a/app_vue/src/assets/images/generic-material.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app_vue/src/assets/images/cardboard.svg b/app_vue/src/assets/images/materialTypeIcons/cardboard.svg similarity index 100% rename from app_vue/src/assets/images/cardboard.svg rename to app_vue/src/assets/images/materialTypeIcons/cardboard.svg diff --git a/app_vue/src/assets/images/compost.svg b/app_vue/src/assets/images/materialTypeIcons/compost.svg similarity index 100% rename from app_vue/src/assets/images/compost.svg rename to app_vue/src/assets/images/materialTypeIcons/compost.svg diff --git a/app_vue/src/assets/images/materialTypeIcons/generic-material.svg b/app_vue/src/assets/images/materialTypeIcons/generic-material.svg new file mode 100644 index 00000000..b2110cbd --- /dev/null +++ b/app_vue/src/assets/images/materialTypeIcons/generic-material.svg @@ -0,0 +1,3 @@ + + + diff --git a/app_vue/src/assets/images/oil-drum.svg b/app_vue/src/assets/images/materialTypeIcons/oil-drum.svg similarity index 100% rename from app_vue/src/assets/images/oil-drum.svg rename to app_vue/src/assets/images/materialTypeIcons/oil-drum.svg diff --git a/app_vue/src/assets/images/plastic.svg b/app_vue/src/assets/images/materialTypeIcons/plastic.svg similarity index 100% rename from app_vue/src/assets/images/plastic.svg rename to app_vue/src/assets/images/materialTypeIcons/plastic.svg diff --git a/app_vue/src/assets/images/trash.svg b/app_vue/src/assets/images/materialTypeIcons/trash.svg similarity index 100% rename from app_vue/src/assets/images/trash.svg rename to app_vue/src/assets/images/materialTypeIcons/trash.svg diff --git a/app_vue/src/assets/stylesheets/_functions.scss b/app_vue/src/assets/stylesheets/_functions.scss index 74123136..730c5eb5 100644 --- a/app_vue/src/assets/stylesheets/_functions.scss +++ b/app_vue/src/assets/stylesheets/_functions.scss @@ -61,11 +61,19 @@ color: #C92828; } +.color-warning-bg { + background-color: #FFF4E5; +} + // text .font-body { @include fontBody; } +.font-body-small { + @include fontBodySmall; +} + // misc .cursor-pointer { cursor: pointer; diff --git a/app_vue/src/components/sensorMap.vue b/app_vue/src/components/sensorMap.vue index d5e7596d..4e792ed5 100644 --- a/app_vue/src/components/sensorMap.vue +++ b/app_vue/src/components/sensorMap.vue @@ -1,35 +1,68 @@ + + + + \ No newline at end of file diff --git a/app_vue/src/components/sensorSideBarFilters.vue b/app_vue/src/components/sensorSideBarFilters.vue index 690153ec..67fbb777 100644 --- a/app_vue/src/components/sensorSideBarFilters.vue +++ b/app_vue/src/components/sensorSideBarFilters.vue @@ -33,7 +33,16 @@ binVolume: [], materialType: [], totalSensors: 0, - showFillLabel: false + showFillLabel: false, + isCollapsed: false, + filterEnabledMap: { + fillRange: false, + group: false, + assetTag: false, + binType: false, + binVolume: false, + matType: false + } }); // watching for store state updates and updating component variables @@ -70,35 +79,65 @@ function updateGroupFilter() { sensorStore.setSelectedGroup(state.selectedGroup); updateSensorsShown(); + + state.filterEnabledMap['group'] = true; } function updateAssetTagFilter() { sensorStore.setSelectedAssetTag(state.selectedAssetTag); updateSensorsShown(); + + if (state.selectedAssetTag.length > 0) { + state.filterEnabledMap['assetTag'] = true; + } else { + state.filterEnabledMap['assetTag'] = false; + } } function updateBinTypeFilter() { sensorStore.setSelectedBinType(state.selectedBinType); updateSensorsShown(); + + if (state.selectedBinType.length > 0) { + state.filterEnabledMap['binType'] = true; + } else { + state.filterEnabledMap['binType'] = false; + } } function updateBinVolumeFilter() { sensorStore.setSelectedBinVolume(state.selectedBinVolume); updateSensorsShown(); + + state.filterEnabledMap['binVolume'] = true; } function updateFillRangeFilter() { sensorStore.setSelectedFillRange(state.selectedFillRange); updateSensorsShown(); + + const isInitial = state.selectedFillRange[0] === 0 && state.selectedFillRange[1] === 100; + if (isInitial) { + state.filterEnabledMap['fillRange'] = false; + } else { + state.filterEnabledMap['fillRange'] = true; + } } function updateMaterialTypeFilter() { sensorStore.setSelectedMaterialType(state.selectedMaterialType); updateSensorsShown(); + + if (state.selectedMaterialType.length > 0) { + state.filterEnabledMap['materialType'] = true; + } else { + state.filterEnabledMap['materialType'] = false; + } } function clearFilters() { sensorStore.clearSelected(); // clear store of selected values + routeStore.clearSensorRoute(); // clear route // clear v-models in form state.selectedGroup = null; state.selectedAssetTag = []; @@ -106,82 +145,104 @@ state.selectedBinVolume = null; state.selectedFillRange = [0, 100]; state.selectedMaterialType = []; + + Object.keys(state.filterEnabledMap).forEach(key => { + state.filterEnabledMap[key] = false; + }) + } + + function getFilterCount() { // returns number of filters in use + const truthArr = Object.values(state.filterEnabledMap).filter(isEnabled => isEnabled); + return truthArr.length; } @@ -207,6 +268,14 @@ &__label { @include fontBody; } + + &__title { + min-width: 164px; + } + + .v-btn { + min-width: 0; + } } diff --git a/app_vue/src/components/sensorSideBarRoutes.vue b/app_vue/src/components/sensorSideBarRoutes.vue index 5975986d..3471fec6 100644 --- a/app_vue/src/components/sensorSideBarRoutes.vue +++ b/app_vue/src/components/sensorSideBarRoutes.vue @@ -1,81 +1,43 @@ @@ -83,64 +45,44 @@ @@ -159,33 +101,11 @@ } .routes-list { - margin-top: 40px; width: 100%; - &__route-container { - border: 1px solid $lightergrey; - border-radius: 20px; - @include fontBodySmall;; - } - - &__route { - margin-bottom: 12px; - &:last-child { - margin-bottom: 0; - } - } - - &__export { - display: flex; - align-items: center; - } - - &__items { - cursor: pointer; - } - - :deep .v-btn--size-default { - min-width: 24px; + &__warning-text { + max-width: 200px; + @include fontBodySmall; } } \ No newline at end of file diff --git a/app_vue/src/components/sensorSidebar.vue b/app_vue/src/components/sensorSidebar.vue index 276c9fcb..c5b751ac 100644 --- a/app_vue/src/components/sensorSidebar.vue +++ b/app_vue/src/components/sensorSidebar.vue @@ -88,11 +88,6 @@ } } - .routes-list { - margin-top: 40px; - width: 100%; - } - .map-updated { display: none; //TODO: switch to flex when needed // display: flex; diff --git a/app_vue/src/data/latestReadingsMock.json b/app_vue/src/data/latestReadingsMock.json index c7f7f217..63a05e27 100644 --- a/app_vue/src/data/latestReadingsMock.json +++ b/app_vue/src/data/latestReadingsMock.json @@ -133,8 +133,8 @@ "id": "265434817", "sensor_type": "solid bin level", "fill_level": 40, - "lat": 43.351197, - "long": -79.347344, + "lat": 43.452197, + "long": -80.147244, "manufacturer": "Sensational Sensors", "bin_name": "Cherry Bin", "address_line1": "99473 Fruit Blvd", diff --git a/app_vue/src/stores/route_store.js b/app_vue/src/stores/route_store.js index ed5de58a..9b260524 100644 --- a/app_vue/src/stores/route_store.js +++ b/app_vue/src/stores/route_store.js @@ -1,79 +1,127 @@ import { defineStore } from 'pinia' +import { getOptimizedRoute } from '@/utils/optimizeRouteHelper'; export const useRouteStore = defineStore('route', { state: () => ({ - sensorRouteList: [], // list of sensor objects part of the route - enableOptimizeRoute: true, // enables optimize route button to be clicked - isRouteOptimized: false + selectedRouteList: [], // list of sensor objects part of the route + isRouteOptimized: false, // when google api has been run on the route and we want to present it + startPointAddress: '6900 Airport Rd Mississauga ON', + endPointAddress: '6900 Airport Rd Mississauga ON', + routeDuration: '', + routeDistance: '', + hasMappedStartEnd: false }), getters: { - getSensorRouteList({sensorRouteList}) { - return sensorRouteList; + getSelectedRouteList({selectedRouteList}) { + return selectedRouteList; }, - getSensorRouteLatLong({sensorRouteList}) { - if (sensorRouteList.length > 0) { - return sensorRouteList.map(sensor => [sensor.lat, sensor.long]); + getSelectedRouteLatLong({selectedRouteList}) { + if (selectedRouteList.length > 0) { + return selectedRouteList.map(sensor => [sensor.lat, sensor.long]); } return []; }, - getEnableOptimizeRoute({enableOptimizeRoute}) { - return enableOptimizeRoute; - }, getIsRouteOptimized({isRouteOptimized}) { return isRouteOptimized; }, + getStartPointAddress({startPointAddress}) { + return startPointAddress; + }, + getEndPointAddress({endPointAddress}) { + return endPointAddress; + }, + getRouteDuration({routeDuration}) { + return routeDuration; + }, + getRouteDistance({routeDistance}) { + return routeDistance; + }, + getHasMappedStartEnd({hasMappedStartEnd}) { + return hasMappedStartEnd; + }, }, actions: { + setStartPoint(value) { + this.startPointAddress = value; + }, + setHasMappedStartEnd(value) { + this.hasMappedStartEnd = value; + }, + setEndPoint(value) { + this.endPointAddress = value; + }, + setRouteDuration(value) { + this.routeDuration = value; + }, + setRouteDistance(value) { + this.routeDistance = value; + }, + setSelectedRouteList(value) { + this.selectedRouteList = value; + }, + updateRouteListWithSensors(sensors) { + this.selectedRouteList = [...sensors]; + }, addSensorToRoute(sensor) { - this.sensorRouteList.push(sensor); - // reset - this.enableOptimizeRoute = true; - this.isRouteOptimized = false; + this.selectedRouteList.push(sensor); }, removeSensorFromRoute(sensor) { - const index = this.sensorRouteList.indexOf(sensor); + const index = this.selectedRouteList.indexOf(sensor); if (index > -1) { - this.sensorRouteList.splice(index, 1); + this.selectedRouteList.splice(index, 1); } - // reset - this.enableOptimizeRoute = true; - this.isRouteOptimized = false; }, clearSensorRoute() { - this.sensorRouteList = []; + this.selectedRouteList = []; // reset - this.enableOptimizeRoute = true; this.isRouteOptimized = false; }, - setEnableOptimizedRoute(value) { - this.enableOptimizeRoute = value; - }, setIsRouteOptimized(value) { this.isRouteOptimized = value; }, - // reorders intermediate points in our sensorRouteList according to a list of given indexes (routeOrder) + isAlreadyInRoute(sensor) { + if (this.getSelectedRouteList.length > 0) { + return !!this.getSelectedRouteList.find(bin => bin.id === sensor.id); + } + return false + }, + // reorders intermediate points in our selectedRouteList according to a list of given indexes (routeOrder) updateWithOptimizedRoute(routeOrder) { // assuming routeOrder looks like [originLocationSensor, midLocationSensor1...midLocationSensor4, destinationLocaitonSensor] // variable to hold our intermediate waypoints for update (no start/end point included) - const intermediates = [...this.sensorRouteList]; - intermediates.shift(); // remove start point - intermediates.pop(); // remove end point + const intermediates = [...this.selectedRouteList]; // refer to google route order and match it to our defined route // copy result to temp let temp = []; - const arrLength = intermediates.length; - for (let i = 0; i < arrLength; i++) { - temp[routeOrder[i]] = intermediates[i]; + for (let i = 0; i < routeOrder.length; i++) { + temp[i] = intermediates[routeOrder[i]]; } // replace intermediate waypoint array with temp - for (let j = 0; j < arrLength; j++) { + for (let j = 0; j < routeOrder.length; j++) { intermediates[j] = temp[j]; } // update saved routelist with new route order - this.sensorRouteList.splice(1, intermediates.length, ...intermediates); + this.selectedRouteList = [...intermediates]; + }, + async googOptimizeRoute() { + const googResponse = await getOptimizedRoute(this.getSelectedRouteList, this.startPointAddress, this.endPointAddress); + if (googResponse && googResponse.routes && googResponse.routes[0]) { + const routeOrder = googResponse.routes[0].optimizedIntermediateWaypointIndex; // [0,3,4] + this.updateWithOptimizedRoute(routeOrder); // update current route + this.setIsRouteOptimized(true); // set flag is optimized to true + + if (googResponse.routes[0].duration) { + this.setRouteDuration(googResponse.routes[0]?.duration); + } + + if (googResponse.routes[0].distanceMeters) { + this.setRouteDistance(googResponse.routes[0]?.distanceMeters); + } + + } } }, }) \ No newline at end of file diff --git a/app_vue/src/utils/formattingHelper.js b/app_vue/src/utils/formattingHelper.js new file mode 100644 index 00000000..3be8ec8d --- /dev/null +++ b/app_vue/src/utils/formattingHelper.js @@ -0,0 +1,15 @@ +export const getMinutesString = (stringSeconds) => { + let duration = stringSeconds.replace('s',''); + + const hours = Math.floor(duration / 3600); + duration = duration % 3600; + const minutes = Math.floor(duration / 60); + + const hoursString = hours ? `${hours} hrs ` : ''; + const minutesString = minutes ? `${minutes} min ` : ''; + return hoursString + minutesString; +}; + +export const getKmFromMeterString = (meters) => { + return (meters / 1000).toFixed(0); +}; \ No newline at end of file diff --git a/app_vue/src/utils/getLatLngFromAddressHelper.js b/app_vue/src/utils/getLatLngFromAddressHelper.js new file mode 100644 index 00000000..0b20710f --- /dev/null +++ b/app_vue/src/utils/getLatLngFromAddressHelper.js @@ -0,0 +1,20 @@ +import axios from 'axios'; + + +// uses openstreetmap geocoding (opensource) +// https://nominatim.openstreetmap.org/ui/search.html +export const getLatLng = async (address) => { + const url = `https://nominatim.openstreetmap.org/search?format=json&limit=3&q=${address}`; + try { + const response = await axios.get(url); + if (response && response.data && response.data[0]) { + return [response.data[0].lat, response.data[0].lon]; + } + return []; + } catch(e) { + console.error('error generating lat lng', e); + return []; + } +}; + +export default getLatLng \ No newline at end of file diff --git a/app_vue/src/utils/mapMarkerHelper.js b/app_vue/src/utils/mapMarkerHelper.js index 0d6907cd..0f16dc85 100644 --- a/app_vue/src/utils/mapMarkerHelper.js +++ b/app_vue/src/utils/mapMarkerHelper.js @@ -17,19 +17,20 @@ const getBinIconName = (sensor) => { } export const getMaterialTypeIconURL = (materialType) => { + const directory = 'src/assets/images/materialTypeIcons'; if (!materialType) { return 'src/assets/images/generic-material.png'; } const materialMap = { - 'Cardboard': 'src/assets/images/cardboard.svg', //confirmed - 'Plastic': 'src/assets/images/plastic.svg', // confirmed - 'Compost': 'src/assets/images/compost.svg', // confirmed - 'Diesel': 'src/assets/images/oil-drum.svg', - 'Dyed Diesel': 'src/assets/images/oil-drum.svg', - 'Kerosene': 'src/assets/images/oil-drum.svg', - 'Water': 'src/assets/images/plastic.svg', // TODO: confirm water icon - 'Gas Oil': 'src/assets/images/oil-drum.svg', - 'Mixed waste': 'src/assets/images/trash.svg' + 'Cardboard': `${directory}/cardboard.svg`, //confirmed + 'Plastic': `${directory}/plastic.svg`, // confirmed + 'Compost': `${directory}/compost.svg`, // confirmed + 'Diesel': `${directory}/oil-drum.svg`, + 'Dyed Diesel': `${directory}/oil-drum.svg`, + 'Kerosene': `${directory}/oil-drum.svg`, + 'Water': `${directory}/plastic.svg`, // TODO: confirm water icon + 'Gas Oil': `${directory}/oil-drum.svg`, + 'Mixed waste': `${directory}/trash.svg` }; return materialMap[materialType] || 'src/assets/images/generic-material.png'; } @@ -44,25 +45,26 @@ export const getIconAndProgressColor = (sensor) => { const RED = '#C92828'; const GREEN = 'green'; const ORANGE = '#F07F2D'; + const iconDirectory = 'src/assets/images/binMarkerIcons'; switch (iconName) { case 'error': - iconUrl = 'src/assets/images/alert-bin.png'; + iconUrl = `${iconDirectory}/alert-bin.png`; linearProgressColor = RED; break; case 'full': - iconUrl = 'src/assets/images/full-bin.png'; + iconUrl = `${iconDirectory}/full-bin.png`; linearProgressColor = RED; break; case 'half-full': - iconUrl = 'src/assets/images/half-full-bin.png'; + iconUrl = `${iconDirectory}/half-full-bin.png`; linearProgressColor = ORANGE; break; case 'empty': - iconUrl = 'src/assets/images/empty-bin.png'; + iconUrl = `${iconDirectory}/empty-bin.png`; linearProgressColor = GREEN; break; default: // default is empty bin - iconUrl = 'src/assets/images/empty-bin.png'; + iconUrl = `${iconDirectory}/empty-bin.png`; linearProgressColor = GREEN; } return { iconUrl, linearProgressColor }; diff --git a/app_vue/src/utils/optimizeRouteHelper.js b/app_vue/src/utils/optimizeRouteHelper.js index ee149e55..748e9cc3 100644 --- a/app_vue/src/utils/optimizeRouteHelper.js +++ b/app_vue/src/utils/optimizeRouteHelper.js @@ -1,14 +1,17 @@ import axios from 'axios'; -const getGoogPayload = (sensorRouteList) => { - const waypointArr = [...sensorRouteList]; // ensures original variable isnt modified - const origin = waypointArr.shift(); // grab first sensor in route - const destination = waypointArr.pop(); // grab last sensor in route - +const getGoogPayload = (selectedRouteList, originAddress, destinationAddress) => { + const origin = { + "address": originAddress + }; + const destination = { + "address": destinationAddress + }; + const waypointArr = [...selectedRouteList]; // ensures original variable isnt modified const intermediates = waypointArr.map(waypoint => { return { - "location":{ - "latLng":{ + "location": { + "latLng": { "latitude": waypoint.lat, "longitude": waypoint.long } @@ -16,24 +19,10 @@ const getGoogPayload = (sensorRouteList) => { } }); - const data = { - "origin": { // specify starting point - "location":{ - "latLng":{ - "latitude": origin.lat, - "longitude": origin.long - } - } - }, + return { + origin, intermediates, - "destination":{ // specify ending point - "location":{ - "latLng":{ - "latitude": destination.lat, - "longitude": destination.long - } - } - }, + destination, "travelMode": "DRIVE", "routingPreference": "TRAFFIC_UNAWARE", "computeAlternativeRoutes": false, @@ -44,24 +33,22 @@ const getGoogPayload = (sensorRouteList) => { }, "languageCode": "en-US", "units": "IMPERIAL", - "optimizeWaypointOrder": "true", // note this special feature being true charges us advanced:route pricing + "optimizeWaypointOrder": "true" }; - return data; }; // google api call to optimize route // https://developers.google.com/maps/documentation/routes/opt-way -export const getOptimizedRoute = async (sensorRouteList) => { +export const getOptimizedRoute = async (selectedRouteList, originAddress, destinationAddress) => { - if (sensorRouteList && sensorRouteList.length === 0) { + if (selectedRouteList && selectedRouteList.length === 0) { return; } // google headers - const googApiKey = 'AIzaSyAgixnED4py56GFy-b2hlfYgofEyISUjSo'; //TODO: move to .env + const googApiKey = 'AIzaSyAgixnED4py56GFy-b2hlfYgofEyISUjSo'; const googFieldMask = 'routes.duration,routes.distanceMeters,routes.optimizedIntermediateWaypointIndex'; const URL = 'https://routes.googleapis.com/directions/v2:computeRoutes'; - const data = getGoogPayload(sensorRouteList); - + const data = getGoogPayload(selectedRouteList, originAddress, destinationAddress); const config = { headers: { 'Content-Type': 'application/json', @@ -71,6 +58,7 @@ export const getOptimizedRoute = async (sensorRouteList) => { }; try { + // if there is an empty object returned, then there is an incorrect lat/long passed const response = await axios.post(URL, data, config); if (response) { return response.data; diff --git a/sensational_sensors/main.py b/sensational_sensors/main.py index 5a2c11e3..4e0549b9 100644 --- a/sensational_sensors/main.py +++ b/sensational_sensors/main.py @@ -151,8 +151,8 @@ class Sensor(BaseModel): "sensor_type": SensorType.LIQUID_BIN_LEVEL, "fill_level": 40, "sim": "81kai41j8", - "lat": 43.351197, - "long": -79.347344, + "lat": 43.452197, + "long": -80.147244, "man": "Sensational Sensors", "bin_name": "Cherry Bin", "address_line1": "99473 Fruit Blvd",