From a4da09784800667cd3445ad0d1a2736a6d75104e Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 13 Sep 2023 09:42:19 -0400 Subject: [PATCH 01/25] Add backend simulations viewset and tasks --- uvdat/core/tasks/simulations.py | 60 +++++++++++++++++++++++++++++++++ uvdat/core/views.py | 19 +++++++++++ uvdat/urls.py | 1 + 3 files changed, 80 insertions(+) create mode 100644 uvdat/core/tasks/simulations.py diff --git a/uvdat/core/tasks/simulations.py b/uvdat/core/tasks/simulations.py new file mode 100644 index 00000000..9e198b3e --- /dev/null +++ b/uvdat/core/tasks/simulations.py @@ -0,0 +1,60 @@ +import re +from uvdat.core.models import Dataset + + +def flood_scenario_1(network_dataset, elevation_dataset, flood_dataset): + print(network_dataset, elevation_dataset, flood_dataset) + + +AVAILABLE_SIMULATIONS = [ + { + 'id': 1, + 'name': 'Flood Scenario 1', + 'description': ''' + Provide a network dataset, elevation dataset, and flood dataset + to determine which network nodes go out of service + when the target flood occurs. + ''', + 'output_type': 'node_failure_animation', + 'func': flood_scenario_1, + 'args': [ + { + 'name': 'network_dataset', + 'type': Dataset, + 'options_query': {'network': True}, + }, + { + 'name': 'elevation_dataset', + 'type': Dataset, + 'options_query': {'category': 'elevation'}, + }, + { + 'name': 'flood_dataset', + 'type': Dataset, + 'options_query': {'category': 'flood'}, + }, + ], + } +] + + +def get_available_simulations(city_id: int): + sims = [] + for available in AVAILABLE_SIMULATIONS: + available = available.copy() + available['description'] = re.sub(r'\n\s+', ' ', available['description']) + available['args'] = [ + { + 'name': a['name'], + 'options': list( + a['type'].objects.filter( + city__id=city_id, + **a['options_query'], + ) + ), + } + for a in available['args'] + ] + del available['func'] + sims.append(available) + return sims diff --git a/uvdat/core/views.py b/uvdat/core/views.py index 44acc21e..dd1626b8 100644 --- a/uvdat/core/views.py +++ b/uvdat/core/views.py @@ -21,6 +21,7 @@ from uvdat.core.tasks.charts import add_gcc_chart_datum from uvdat.core.tasks.conversion import convert_raw_data from uvdat.core.tasks.networks import network_gcc +from uvdat.core.tasks.simulations import get_available_simulations class CityViewSet(ModelViewSet): @@ -169,3 +170,21 @@ def clear(self, request, **kwargs): chart.chart_data = {} chart.save() return HttpResponse(status=200) + + +class SimulationViewSet(GenericViewSet): + # Not based on a database model; + # Available Simulations must be hard-coded and associated with a function + + @action( + detail=False, + methods=['get'], + url_path="available", + ) + def list_available(self, request, **kwargs): + city_id = kwargs.get('city') + sims = get_available_simulations(city_id) + return HttpResponse( + json.dumps(sims), + status=200, + ) diff --git a/uvdat/urls.py b/uvdat/urls.py index 311ea5ad..3f0ce3fa 100644 --- a/uvdat/urls.py +++ b/uvdat/urls.py @@ -18,6 +18,7 @@ router.register(r'datasets', views.DatasetViewSet, basename='datasets') router.register(r'cities', views.CityViewSet, basename='cities') router.register(r'charts', views.ChartViewSet, basename='charts') +router.register(r'simulations', views.SimulationViewSet, basename='simulations') urlpatterns = [ path('accounts/', include('allauth.urls')), From e7e0a2d9541ec72a3c80e0585425203aa75cdcc1 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 13 Sep 2023 09:42:44 -0400 Subject: [PATCH 02/25] Add available simulations list to UI --- web/src/api/rest.ts | 15 +++++++- web/src/components/MainDrawerContents.vue | 46 ++++++++++++++++++++++- web/src/store.ts | 5 ++- web/src/types.ts | 11 ++++++ 4 files changed, 74 insertions(+), 3 deletions(-) diff --git a/web/src/api/rest.ts b/web/src/api/rest.ts index 59b89da3..d36a31a1 100644 --- a/web/src/api/rest.ts +++ b/web/src/api/rest.ts @@ -1,5 +1,12 @@ import { apiClient } from "./auth"; -import { City, Dataset, NetworkNode, RasterData, Chart } from "@/types"; +import { + City, + Dataset, + NetworkNode, + RasterData, + Chart, + Simulation, +} from "@/types"; export async function getCities(): Promise { return (await apiClient.get("cities")).data.results; @@ -9,6 +16,12 @@ export async function getCityCharts(cityId: number): Promise { return (await apiClient.get(`charts?city=${cityId}`)).data.results; } +export async function getCitySimulations( + cityId: number +): Promise { + return (await apiClient.get(`simulations/available/?city=${cityId}`)).data; +} + export async function getDataset(datasetId: number): Promise { return (await apiClient.get(`datasets/${datasetId}`)).data; } diff --git a/web/src/components/MainDrawerContents.vue b/web/src/components/MainDrawerContents.vue index dcc58754..72cf3faa 100644 --- a/web/src/components/MainDrawerContents.vue +++ b/web/src/components/MainDrawerContents.vue @@ -5,11 +5,13 @@ import { currentDataset, activeChart, availableCharts, + activeSimulation, + availableSimulations, selectedDatasetIds, } from "@/store"; import { ref, computed, onMounted } from "vue"; import { addDatasetLayerToMap } from "@/utils.js"; -import { getCityCharts } from "@/api/rest"; +import { getCityCharts, getCitySimulations } from "@/api/rest"; import { updateVisibleLayers } from "../utils"; export default { @@ -91,6 +93,18 @@ export default { activeChart.value = chart; } + function fetchSimulations() { + activeSimulation.value = undefined; + getCitySimulations(currentCity.value.id).then((sims) => { + console.log(sims); + availableSimulations.value = sims; + }); + } + + function activateSimulation(sim) { + activeSimulation.value = sim; + } + onMounted(fetchCharts); return { @@ -108,6 +122,10 @@ export default { availableCharts, fetchCharts, activateChart, + activeSimulation, + availableSimulations, + fetchSimulations, + activateSimulation, }; }, }; @@ -212,6 +230,32 @@ export default { + + + + mdi-refresh + Available Simulations + + + No simulations available. + + + {{ sim.name }} + + {{ sim.description }} + + + + + diff --git a/web/src/store.ts b/web/src/store.ts index 4c5797ac..bf5fc192 100644 --- a/web/src/store.ts +++ b/web/src/store.ts @@ -19,8 +19,11 @@ export const map = ref(); export const mapLayers = ref(); export const showMapBaseLayer = ref(true); export const rasterTooltip = ref(); + export const activeChart = ref(); -export const availableCharts = ref(); +export const availableCharts = ref([]); +export const activeSimulation = ref(); +export const availableSimulations = ref([]); export const networkVis = ref(); export const deactivatedNodes = ref([]); diff --git a/web/src/types.ts b/web/src/types.ts index f7407b14..1707a756 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -67,3 +67,14 @@ export interface Chart { datasets: object[]; }; } + +export interface Simulation { + id: number; + name: string; + description: string; + output_type: string; + args: { + name: string; + options: object[]; + }[]; +} From b85f92811e264bce78f1dd21743940a4c81afa22 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 13 Sep 2023 09:53:43 -0400 Subject: [PATCH 03/25] Add refresh button to Available Datasets panel and reorder panels (follow panel pattern) --- uvdat/core/views.py | 8 ++- web/src/api/rest.ts | 6 +- web/src/components/MainDrawerContents.vue | 79 +++++++++++++---------- 3 files changed, 58 insertions(+), 35 deletions(-) diff --git a/uvdat/core/views.py b/uvdat/core/views.py index dd1626b8..e1e3931e 100644 --- a/uvdat/core/views.py +++ b/uvdat/core/views.py @@ -30,10 +30,16 @@ class CityViewSet(ModelViewSet): class DatasetViewSet(ModelViewSet, LargeImageFileDetailMixin): - queryset = Dataset.objects.all() serializer_class = DatasetSerializer FILE_FIELD_NAME = 'raster_file' + def get_queryset(self): + city_id = self.request.query_params.get('city') + if city_id: + return Dataset.objects.filter(city__id=city_id) + else: + return Dataset.objects.all() + @action(detail=True, methods=['get']) def regions(self, request, **kwargs): dataset = self.get_object() diff --git a/web/src/api/rest.ts b/web/src/api/rest.ts index d36a31a1..7abb615a 100644 --- a/web/src/api/rest.ts +++ b/web/src/api/rest.ts @@ -12,6 +12,10 @@ export async function getCities(): Promise { return (await apiClient.get("cities")).data.results; } +export async function getCityDatasets(cityId: number): Promise { + return (await apiClient.get(`datasets?city=${cityId}`)).data.results; +} + export async function getCityCharts(cityId: number): Promise { return (await apiClient.get(`charts?city=${cityId}`)).data.results; } @@ -19,7 +23,7 @@ export async function getCityCharts(cityId: number): Promise { export async function getCitySimulations( cityId: number ): Promise { - return (await apiClient.get(`simulations/available/?city=${cityId}`)).data; + return (await apiClient.get(`simulations/available?city=${cityId}`)).data; } export async function getDataset(datasetId: number): Promise { diff --git a/web/src/components/MainDrawerContents.vue b/web/src/components/MainDrawerContents.vue index 72cf3faa..f646b491 100644 --- a/web/src/components/MainDrawerContents.vue +++ b/web/src/components/MainDrawerContents.vue @@ -11,7 +11,7 @@ import { } from "@/store"; import { ref, computed, onMounted } from "vue"; import { addDatasetLayerToMap } from "@/utils.js"; -import { getCityCharts, getCitySimulations } from "@/api/rest"; +import { getCityDatasets, getCityCharts, getCitySimulations } from "@/api/rest"; import { updateVisibleLayers } from "../utils"; export default { @@ -19,7 +19,7 @@ export default { draggable, }, setup() { - const openPanels = ref([0]); + const openPanels = ref([1]); const openCategories = ref([0]); const availableLayerTree = computed(() => { const groupKey = "category"; @@ -41,6 +41,13 @@ export default { }); const activeLayerTableHeaders = [{ text: "Name", value: "name" }]; + function fetchDatasets() { + currentDataset.value = undefined; + getCityDatasets(currentCity.value.id).then((datasets) => { + currentCity.value.datasets = datasets; + }); + } + function updateActiveDatasets() { if (selectedDatasetIds.value.length) { openPanels.value = [0, 1]; @@ -106,10 +113,12 @@ export default { } onMounted(fetchCharts); + onMounted(fetchSimulations); return { selectedDatasetIds, currentCity, + fetchDatasets, openPanels, openCategories, toggleDataset, @@ -133,7 +142,40 @@ export default { diff --git a/web/src/components/MainDrawerContents.vue b/web/src/components/MainDrawerContents.vue index f646b491..bdb08df7 100644 --- a/web/src/components/MainDrawerContents.vue +++ b/web/src/components/MainDrawerContents.vue @@ -103,7 +103,6 @@ export default { function fetchSimulations() { activeSimulation.value = undefined; getCitySimulations(currentCity.value.id).then((sims) => { - console.log(sims); availableSimulations.value = sims; }); } diff --git a/web/src/components/SimulationsPanel.vue b/web/src/components/SimulationsPanel.vue new file mode 100644 index 00000000..c13e1243 --- /dev/null +++ b/web/src/components/SimulationsPanel.vue @@ -0,0 +1,84 @@ + + + + + From e5e071759232ea485325b4d3d10c583116c2b05e Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 13 Sep 2023 11:08:10 -0400 Subject: [PATCH 10/25] Add client POST requests functions --- web/src/api/rest.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/web/src/api/rest.ts b/web/src/api/rest.ts index 7abb615a..ccafae75 100644 --- a/web/src/api/rest.ts +++ b/web/src/api/rest.ts @@ -9,15 +9,15 @@ import { } from "@/types"; export async function getCities(): Promise { - return (await apiClient.get("cities")).data.results; + return (await apiClient.get("cities")).data; } export async function getCityDatasets(cityId: number): Promise { - return (await apiClient.get(`datasets?city=${cityId}`)).data.results; + return (await apiClient.get(`datasets?city=${cityId}`)).data; } export async function getCityCharts(cityId: number): Promise { - return (await apiClient.get(`charts?city=${cityId}`)).data.results; + return (await apiClient.get(`charts?city=${cityId}`)).data; } export async function getCitySimulations( @@ -66,5 +66,9 @@ export async function getRasterData(datasetId: number): Promise { } export async function clearChart(chartId: number) { - await apiClient.get(`charts/${chartId}/clear/`); + await apiClient.post(`charts/${chartId}/clear/`); +} + +export async function runSimulation(simulationId: number, args: object) { + await apiClient.post(`simulations/run/${simulationId}/`, args); } From 7acf2c4d62553ab0b4455fb347f4362f44af100e Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 13 Sep 2023 11:23:00 -0400 Subject: [PATCH 11/25] Add SimulationResult model --- uvdat/core/admin.py | 7 ++- .../migrations/0008_simulation_results.py | 43 +++++++++++++++++++ uvdat/core/models.py | 6 +++ uvdat/core/serializers.py | 8 +++- uvdat/core/tasks/conversion.py | 2 +- uvdat/core/tasks/simulations.py | 19 +++++--- 6 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 uvdat/core/migrations/0008_simulation_results.py diff --git a/uvdat/core/admin.py b/uvdat/core/admin.py index 647aa0aa..6d138ccc 100644 --- a/uvdat/core/admin.py +++ b/uvdat/core/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from uvdat.core.models import Chart, City, Dataset, NetworkNode, Region +from uvdat.core.models import Chart, City, Dataset, NetworkNode, Region, SimulationResult class CityAdmin(admin.ModelAdmin): @@ -26,8 +26,13 @@ class ChartAdmin(admin.ModelAdmin): list_display = ['id', 'name'] +class SimulationResultAdmin(admin.ModelAdmin): + list_display = ['id', 'simulation_id', 'input_args'] + + admin.site.register(City, CityAdmin) admin.site.register(Dataset, DatasetAdmin) admin.site.register(NetworkNode, NetworkNodeAdmin) admin.site.register(Region, RegionAdmin) admin.site.register(Chart, ChartAdmin) +admin.site.register(SimulationResult, SimulationResultAdmin) diff --git a/uvdat/core/migrations/0008_simulation_results.py b/uvdat/core/migrations/0008_simulation_results.py new file mode 100644 index 00000000..e5c14a23 --- /dev/null +++ b/uvdat/core/migrations/0008_simulation_results.py @@ -0,0 +1,43 @@ +# Generated by Django 4.1 on 2023-09-13 15:11 + +from django.db import migrations, models +import django_extensions.db.fields + + +class Migration(migrations.Migration): + dependencies = [ + ('core', '0007_charts'), + ] + + operations = [ + migrations.CreateModel( + name='SimulationResult', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name='ID' + ), + ), + ( + 'created', + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name='created' + ), + ), + ( + 'modified', + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name='modified' + ), + ), + ('simulation_id', models.IntegerField()), + ('input_args', models.JSONField(blank=True, null=True)), + ('output_data', models.JSONField(blank=True, null=True)), + ], + options={ + 'get_latest_by': 'modified', + 'abstract': False, + }, + ), + ] diff --git a/uvdat/core/models.py b/uvdat/core/models.py index 82a247e2..4d4f4ea5 100644 --- a/uvdat/core/models.py +++ b/uvdat/core/models.py @@ -65,3 +65,9 @@ class Chart(models.Model): metadata = models.JSONField(blank=True, null=True) style = models.JSONField(blank=True, null=True) clearable = models.BooleanField(default=False) + + +class SimulationResult(TimeStampedModel, models.Model): + simulation_id = models.IntegerField() + input_args = models.JSONField(blank=True, null=True) + output_data = models.JSONField(blank=True, null=True) diff --git a/uvdat/core/serializers.py b/uvdat/core/serializers.py index 9e2dd212..ebf92c98 100644 --- a/uvdat/core/serializers.py +++ b/uvdat/core/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from uvdat.core.models import Chart, City, Dataset, NetworkNode +from uvdat.core.models import Chart, City, Dataset, NetworkNode, SimulationResult class NetworkNodeSerializer(serializers.ModelSerializer): @@ -42,3 +42,9 @@ def get_center(self, obj): class Meta: model = City fields = '__all__' + + +class SimulationResultSerializer(serializers.ModelSerializer): + class Meta: + model = SimulationResult + fields = '__all__' diff --git a/uvdat/core/tasks/conversion.py b/uvdat/core/tasks/conversion.py index 873766c0..d0e3b7d5 100644 --- a/uvdat/core/tasks/conversion.py +++ b/uvdat/core/tasks/conversion.py @@ -1,5 +1,5 @@ -import os import json +import os from pathlib import Path import tempfile import zipfile diff --git a/uvdat/core/tasks/simulations.py b/uvdat/core/tasks/simulations.py index e92bd8b7..50adabd8 100644 --- a/uvdat/core/tasks/simulations.py +++ b/uvdat/core/tasks/simulations.py @@ -1,11 +1,12 @@ import re -from uvdat.core.models import Dataset -from uvdat.core.serializers import DatasetSerializer +from uvdat.core.models import Dataset, SimulationResult +from uvdat.core.serializers import DatasetSerializer, SimulationResultSerializer -def flood_scenario_1(network_dataset, elevation_dataset, flood_dataset): + +def flood_scenario_1(result, network_dataset, elevation_dataset, flood_dataset): disabled_nodes = [] - print(network_dataset, elevation_dataset, flood_dataset) + print(result, network_dataset, elevation_dataset, flood_dataset) return disabled_nodes @@ -69,5 +70,13 @@ def get_available_simulations(city_id: int): def run_simulation(simulation_id: int, **kwargs): simulation_matches = [s for s in AVAILABLE_SIMULATIONS if s['id'] == simulation_id] if len(simulation_matches) > 0: + sim_result, created = SimulationResult.objects.get_or_create( + simulation_id=simulation_id, input_args=kwargs + ) + sim_result.output_data = None + sim_result.save() + simulation = simulation_matches[0] - return simulation['func'](**kwargs) + simulation['func'](sim_result, **kwargs) + return SimulationResultSerializer(sim_result).data + return f"No simulation found with id {simulation_id}." From deb898b0baeb222dacd1f61b1ca877238732f01e Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 13 Sep 2023 11:52:07 -0400 Subject: [PATCH 12/25] Upgrade dependencies to eliminate errors from vuetify --- web/package-lock.json | 68 +++++++++++++------------------------------ web/package.json | 6 ++-- web/src/App.vue | 2 +- 3 files changed, 24 insertions(+), 52 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 931e1d9b..5458da78 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,17 +13,15 @@ "axios": "^1.4.0", "buffer": "^6.0.3", "core-js": "^3.8.3", - "geojson-vt": "^3.2.1", "maplibre-gl": "^2.4.0", "ol": "^7.3.0", - "ol-geojson-vt": "^1.0.1", "openlayers": "^4.6.5", "shpjs": "^4.0.4", "sortablejs": "^1.15.0", "vue": "^3.2.13", "vue-chartjs": "^5.2.0", "vuedraggable": "^4.1.0", - "vuetify": "^3.2.2" + "vuetify": "^3.3.16" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.4.0", @@ -40,7 +38,7 @@ "prettier": "^2.4.1", "sass": "^1.32.7", "sass-loader": "^12.0.0", - "typescript": "~4.5.5" + "typescript": "^5.0.0" } }, "node_modules/@achrinza/node-ipc": { @@ -10392,19 +10390,6 @@ "url": "https://opencollective.com/openlayers" } }, - "node_modules/ol-geojson-vt": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ol-geojson-vt/-/ol-geojson-vt-1.0.1.tgz", - "integrity": "sha512-M6/8cpNHfFMmwKHKDbFjmlnkrygE/ybpxsJ3/56NQGmrj8DHjvWk8uOO3QPQ6o3hBJIUfmksdcucWNa8m5TXuw==", - "dependencies": { - "geojson-vt": "^2.4.0" - } - }, - "node_modules/ol-geojson-vt/node_modules/geojson-vt": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-2.4.0.tgz", - "integrity": "sha512-wPK9naAo41/NZnQ3Prl8T5TS8jH4XhV0u3kahjeTMQYkZEqpEnuvVVC9TpmBK5pOONiu6wlDiqtjIoiDvtjssw==" - }, "node_modules/ol-mapbox-style": { "version": "9.7.0", "resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-9.7.0.tgz", @@ -13813,16 +13798,16 @@ } }, "node_modules/typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", - "dev": true, + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uglify-js": { @@ -14379,9 +14364,9 @@ "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==" }, "node_modules/vuetify": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.2.2.tgz", - "integrity": "sha512-syFfeZVH6dycltqVCx4tDn68fR3r697+Jt7vJW1l3i9a5ClnwpdRBWtE6dt2bjClS2K/VpWYt+rAsLiG7sGU/g==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.3.16.tgz", + "integrity": "sha512-vVfUroT5lSU+yOZAf7JlGm2j3dCR4BQml8Al27+/Uh0YW+97/CyUqOzo6IzMCt/bXLvnRerJ5fCAJtQYWnwfLA==", "engines": { "node": "^12.20 || >=14.13" }, @@ -14390,12 +14375,16 @@ "url": "https://github.com/sponsors/johnleider" }, "peerDependencies": { + "typescript": ">=4.7", "vite-plugin-vuetify": "^1.0.0-alpha.12", "vue": "^3.2.0", "vue-i18n": "^9.0.0", "webpack-plugin-vuetify": "^2.0.0-alpha.11" }, "peerDependenciesMeta": { + "typescript": { + "optional": true + }, "vite-plugin-vuetify": { "optional": true }, @@ -23075,21 +23064,6 @@ "rbush": "^3.0.1" } }, - "ol-geojson-vt": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ol-geojson-vt/-/ol-geojson-vt-1.0.1.tgz", - "integrity": "sha512-M6/8cpNHfFMmwKHKDbFjmlnkrygE/ybpxsJ3/56NQGmrj8DHjvWk8uOO3QPQ6o3hBJIUfmksdcucWNa8m5TXuw==", - "requires": { - "geojson-vt": "^2.4.0" - }, - "dependencies": { - "geojson-vt": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-2.4.0.tgz", - "integrity": "sha512-wPK9naAo41/NZnQ3Prl8T5TS8jH4XhV0u3kahjeTMQYkZEqpEnuvVVC9TpmBK5pOONiu6wlDiqtjIoiDvtjssw==" - } - } - }, "ol-mapbox-style": { "version": "9.7.0", "resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-9.7.0.tgz", @@ -25666,10 +25640,10 @@ } }, "typescript": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", - "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", - "dev": true + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "devOptional": true }, "uglify-js": { "version": "2.8.29", @@ -26091,9 +26065,9 @@ } }, "vuetify": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.2.2.tgz", - "integrity": "sha512-syFfeZVH6dycltqVCx4tDn68fR3r697+Jt7vJW1l3i9a5ClnwpdRBWtE6dt2bjClS2K/VpWYt+rAsLiG7sGU/g==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.3.16.tgz", + "integrity": "sha512-vVfUroT5lSU+yOZAf7JlGm2j3dCR4BQml8Al27+/Uh0YW+97/CyUqOzo6IzMCt/bXLvnRerJ5fCAJtQYWnwfLA==", "requires": {} }, "walk": { diff --git a/web/package.json b/web/package.json index 871026b2..c57694c7 100644 --- a/web/package.json +++ b/web/package.json @@ -14,17 +14,15 @@ "axios": "^1.4.0", "buffer": "^6.0.3", "core-js": "^3.8.3", - "geojson-vt": "^3.2.1", "maplibre-gl": "^2.4.0", "ol": "^7.3.0", - "ol-geojson-vt": "^1.0.1", "openlayers": "^4.6.5", "shpjs": "^4.0.4", "sortablejs": "^1.15.0", "vue": "^3.2.13", "vue-chartjs": "^5.2.0", "vuedraggable": "^4.1.0", - "vuetify": "^3.2.2" + "vuetify": "^3.3.16" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.4.0", @@ -41,7 +39,7 @@ "prettier": "^2.4.1", "sass": "^1.32.7", "sass-loader": "^12.0.0", - "typescript": "~4.5.5" + "typescript": "^5.0.0" }, "eslintConfig": { "root": true, diff --git a/web/src/App.vue b/web/src/App.vue index af387b8c..1592961a 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -96,7 +96,7 @@ export default defineComponent({ permanent width="250" location="right" - class="main-area drawer options-drawer" + class="main-area drawer" > From 07c3d1565fb5188d316e0c77d5c19b25f54e3e16 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 13 Sep 2023 12:34:17 -0400 Subject: [PATCH 13/25] Get SimulationResult objects related to a simulation type --- uvdat/core/views.py | 19 ++++++++++++++++++- web/src/api/rest.ts | 6 +++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/uvdat/core/views.py b/uvdat/core/views.py index 24d00f8d..32c06b63 100644 --- a/uvdat/core/views.py +++ b/uvdat/core/views.py @@ -11,12 +11,13 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet, ModelViewSet, mixins -from uvdat.core.models import Chart, City, Dataset, Region +from uvdat.core.models import Chart, City, Dataset, Region, SimulationResult from uvdat.core.serializers import ( ChartSerializer, CitySerializer, DatasetSerializer, NetworkNodeSerializer, + SimulationResultSerializer, ) from uvdat.core.tasks.charts import add_gcc_chart_datum from uvdat.core.tasks.conversion import convert_raw_data @@ -195,6 +196,22 @@ def list_available(self, request, **kwargs): status=200, ) + @action( + detail=False, + methods=['get'], + url_path=r'(?P[\d*]+)/results', + ) + def list_results(self, request, simulation_id: int, **kwargs): + return HttpResponse( + json.dumps( + list( + SimulationResultSerializer(s).data + for s in SimulationResult.objects.filter(simulation_id=int(simulation_id)).all() + ) + ), + status=200, + ) + @action( detail=False, methods=['post'], diff --git a/web/src/api/rest.ts b/web/src/api/rest.ts index ccafae75..f9628eb2 100644 --- a/web/src/api/rest.ts +++ b/web/src/api/rest.ts @@ -70,5 +70,9 @@ export async function clearChart(chartId: number) { } export async function runSimulation(simulationId: number, args: object) { - await apiClient.post(`simulations/run/${simulationId}/`, args); + return (await apiClient.post(`simulations/run/${simulationId}/`, args)).data; +} + +export async function getSimulationResults(simulationId: number) { + return (await apiClient.get(`simulations/${simulationId}/results`)).data; } From dad8382de708a58bdd5e5c6a17fcfc10ae2cb65c Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 13 Sep 2023 12:34:38 -0400 Subject: [PATCH 14/25] Show existing SimulationResult objects and their input args --- web/src/components/MainDrawerContents.vue | 4 +- web/src/components/SimulationsPanel.vue | 146 ++++++++++++++++++---- 2 files changed, 121 insertions(+), 29 deletions(-) diff --git a/web/src/components/MainDrawerContents.vue b/web/src/components/MainDrawerContents.vue index bdb08df7..6c2872f0 100644 --- a/web/src/components/MainDrawerContents.vue +++ b/web/src/components/MainDrawerContents.vue @@ -9,7 +9,7 @@ import { availableSimulations, selectedDatasetIds, } from "@/store"; -import { ref, computed, onMounted } from "vue"; +import { ref, computed, onMounted, watch } from "vue"; import { addDatasetLayerToMap } from "@/utils.js"; import { getCityDatasets, getCityCharts, getCitySimulations } from "@/api/rest"; import { updateVisibleLayers } from "../utils"; @@ -78,7 +78,6 @@ export default { ) { currentDataset.value = undefined; } - updateActiveDatasets(); } function reorderLayers() { @@ -113,6 +112,7 @@ export default { onMounted(fetchCharts); onMounted(fetchSimulations); + watch(selectedDatasetIds, updateActiveDatasets); return { selectedDatasetIds, diff --git a/web/src/components/SimulationsPanel.vue b/web/src/components/SimulationsPanel.vue index c13e1243..a2c3359b 100644 --- a/web/src/components/SimulationsPanel.vue +++ b/web/src/components/SimulationsPanel.vue @@ -1,32 +1,81 @@ + + diff --git a/web/src/components/SimulationsPanel.vue b/web/src/components/SimulationsPanel.vue index a2c3359b..eed80045 100644 --- a/web/src/components/SimulationsPanel.vue +++ b/web/src/components/SimulationsPanel.vue @@ -2,14 +2,19 @@ import { ref, watch } from "vue"; import { activeSimulation, currentCity, selectedDatasetIds } from "@/store"; import { getSimulationResults, runSimulation } from "@/api/rest"; +import NodeFailureAnimation from "./NodeFailureAnimation.vue"; export default { + components: { + NodeFailureAnimation, + }, setup() { const tab = ref(); const activeResult = ref(); const inputForm = ref(); const selectedInputs = ref({}); const availableResults = ref([]); + const outputPoll = ref(); function run() { inputForm.value.validate().then(({ valid }) => { @@ -59,12 +64,34 @@ export default { selectedDatasetIds.value = [dataset.id, ...selectedDatasetIds.value]; } + function pollForActiveDatasetOutput() { + if (!availableResults.value) { + clearInterval(outputPoll.value); + outputPoll.value = undefined; + } + const targetResult = availableResults.value.find( + (r) => r.id == activeResult.value + ); + if (targetResult && !targetResult.output_data) { + fetchResults(); + } else { + clearInterval(outputPoll.value); + outputPoll.value = undefined; + } + } + watch(tab, () => { if (tab.value === "old") { fetchResults(); } }); + watch(activeResult, () => { + if (activeResult.value && !activeResult.value.output_data) { + outputPoll.value = setInterval(pollForActiveDatasetOutput, 3000); + } + }); + return { activeSimulation, tab, @@ -72,6 +99,7 @@ export default { inputForm, selectedInputs, availableResults, + outputPoll, run, timestampToTitle, resultInputArgs, @@ -120,13 +148,20 @@ export default { hide-details="auto" class="my-1" /> - Run Simulation + + Run Simulation + - +
+ No previous runs of this simulation type exist. +
+ +
+ + Waiting for simulation to complete... +
+
+ Results +
+
+ No nodes are affected in this scenario. +
+ +
+
+ Unknown simulation output type + {{ activeSimulation.output_type }} +
+
@@ -168,9 +231,9 @@ export default { position: absolute; top: 10px; right: 10px; - width: 800px; + width: 600px; max-width: calc(100% - 20px); - max-height: 90%; + max-height: 45%; overflow: auto; } diff --git a/web/src/utils.js b/web/src/utils.js index f6059c50..fc4c5572 100644 --- a/web/src/utils.js +++ b/web/src/utils.js @@ -533,15 +533,7 @@ export function displayRasterTooltip(evt, tooltip, overlay) { } } -export function toggleNodeActive(nodeId, button = null) { - if (deactivatedNodes.value.includes(nodeId)) { - deactivatedNodes.value = deactivatedNodes.value.filter((n) => n !== nodeId); - if (button) button.innerHTML = "Deactivate Node"; - } else { - deactivatedNodes.value.push(nodeId); - if (button) button.innerHTML = "Activate Node"; - } - +export function deactivatedNodesUpdated() { currentNetworkGCC.value = undefined; getNetworkGCC(networkVis.value.id, deactivatedNodes.value).then((gcc) => { currentNetworkGCC.value = gcc; @@ -557,3 +549,14 @@ export function toggleNodeActive(nodeId, button = null) { }); updateNetworkStyle(); } + +export function toggleNodeActive(nodeId, button = null) { + if (deactivatedNodes.value.includes(nodeId)) { + deactivatedNodes.value = deactivatedNodes.value.filter((n) => n !== nodeId); + if (button) button.innerHTML = "Deactivate Node"; + } else { + deactivatedNodes.value.push(nodeId); + if (button) button.innerHTML = "Activate Node"; + } + deactivatedNodesUpdated(); +} From 9814351755bce02b14e7376c855b86e36197cc86 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Fri, 15 Sep 2023 11:03:30 -0400 Subject: [PATCH 18/25] Don't redraw network layer until new gcc is received (avoid flickering) --- web/src/utils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/utils.js b/web/src/utils.js index fc4c5572..0f32e2aa 100644 --- a/web/src/utils.js +++ b/web/src/utils.js @@ -547,7 +547,6 @@ export function deactivatedNodesUpdated() { } }); }); - updateNetworkStyle(); } export function toggleNodeActive(nodeId, button = null) { From d58061d61eb2d7b933b586658ab20681f3c78828 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 20 Sep 2023 19:38:29 -0400 Subject: [PATCH 19/25] Remove redundant model inheritance --- uvdat/core/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uvdat/core/models.py b/uvdat/core/models.py index 4d4f4ea5..7931bcde 100644 --- a/uvdat/core/models.py +++ b/uvdat/core/models.py @@ -4,7 +4,7 @@ from s3_file_field import S3FileField -class City(TimeStampedModel, models.Model): +class City(TimeStampedModel): name = models.CharField(max_length=255, unique=True) center = geo_models.PointField() default_zoom = models.IntegerField(default=10) @@ -13,7 +13,7 @@ class Meta: verbose_name_plural = 'cities' -class Dataset(TimeStampedModel, models.Model): +class Dataset(TimeStampedModel): name = models.CharField(max_length=255, unique=True) description = models.TextField(null=True, blank=True) city = models.ForeignKey(City, on_delete=models.CASCADE, related_name='datasets') @@ -67,7 +67,7 @@ class Chart(models.Model): clearable = models.BooleanField(default=False) -class SimulationResult(TimeStampedModel, models.Model): +class SimulationResult(TimeStampedModel): simulation_id = models.IntegerField() input_args = models.JSONField(blank=True, null=True) output_data = models.JSONField(blank=True, null=True) From 9fb16402df97c2f1cd69d6eb0d41485d12bd4165 Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 20 Sep 2023 20:12:32 -0400 Subject: [PATCH 20/25] Add error message to SimulationResult, add input checking --- .../migrations/0008_simulation_results.py | 3 +- uvdat/core/models.py | 1 + uvdat/core/tasks/simulations.py | 24 +++++++++---- web/src/components/SimulationsPanel.vue | 36 ++++++++++++------- 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/uvdat/core/migrations/0008_simulation_results.py b/uvdat/core/migrations/0008_simulation_results.py index e5c14a23..3c8e63fb 100644 --- a/uvdat/core/migrations/0008_simulation_results.py +++ b/uvdat/core/migrations/0008_simulation_results.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1 on 2023-09-13 15:11 +# Generated by Django 4.1 on 2023-09-20 23:42 from django.db import migrations, models import django_extensions.db.fields @@ -34,6 +34,7 @@ class Migration(migrations.Migration): ('simulation_id', models.IntegerField()), ('input_args', models.JSONField(blank=True, null=True)), ('output_data', models.JSONField(blank=True, null=True)), + ('error_message', models.TextField(blank=True, null=True)), ], options={ 'get_latest_by': 'modified', diff --git a/uvdat/core/models.py b/uvdat/core/models.py index 7931bcde..7fbc5d24 100644 --- a/uvdat/core/models.py +++ b/uvdat/core/models.py @@ -71,3 +71,4 @@ class SimulationResult(TimeStampedModel): simulation_id = models.IntegerField() input_args = models.JSONField(blank=True, null=True) output_data = models.JSONField(blank=True, null=True) + error_message = models.TextField(null=True, blank=True) diff --git a/uvdat/core/tasks/simulations.py b/uvdat/core/tasks/simulations.py index ea9e03e2..3b18730d 100644 --- a/uvdat/core/tasks/simulations.py +++ b/uvdat/core/tasks/simulations.py @@ -1,14 +1,13 @@ -from celery import shared_task - import json -import large_image -from django_large_image import tilesource from pathlib import Path - import re -import shapely import tempfile +from celery import shared_task +from django_large_image import tilesource +import large_image +import shapely + from uvdat.core.models import Dataset, SimulationResult from uvdat.core.serializers import DatasetSerializer, SimulationResultSerializer @@ -48,7 +47,18 @@ def flood_scenario_1(simulation_result_id, network_dataset, elevation_dataset, f elevation_dataset = Dataset.objects.get(id=elevation_dataset) flood_dataset = Dataset.objects.get(id=flood_dataset) except Dataset.DoesNotExist: - return [] + result.error_message = 'Dataset not found.' + result.save() + return None + + if ( + not network_dataset.network + or elevation_dataset.category != 'elevation' + or flood_dataset.category != 'flood' + ): + result.error_message = 'Invalid dataset selected.' + result.save() + return None disabled_nodes = [] network_nodes = network_dataset.network_nodes.all() diff --git a/web/src/components/SimulationsPanel.vue b/web/src/components/SimulationsPanel.vue index eed80045..f86d9e6d 100644 --- a/web/src/components/SimulationsPanel.vue +++ b/web/src/components/SimulationsPanel.vue @@ -46,15 +46,17 @@ export default { const simArg = activeSimulation.value.args.find((a) => a.name === k); if (simArg) { const selectedOption = simArg.options.find((o) => o.id === v); - args.push({ - key: k, - value: selectedOption, - viewable: - !selectedDatasetIds.value.includes(selectedOption.id) && - currentCity.value.datasets.some( - (d) => d.id === selectedOption.id - ), - }); + if (selectedOption) { + args.push({ + key: k, + value: selectedOption, + viewable: + !selectedDatasetIds.value.includes(selectedOption.id) && + currentCity.value.datasets.some( + (d) => d.id === selectedOption.id + ), + }); + } } }); return args; @@ -72,7 +74,11 @@ export default { const targetResult = availableResults.value.find( (r) => r.id == activeResult.value ); - if (targetResult && !targetResult.output_data) { + if ( + targetResult && + !targetResult.output_data && + !targetResult.error_message + ) { fetchResults(); } else { clearInterval(outputPoll.value); @@ -190,13 +196,19 @@ export default {
Waiting for simulation to complete...
-
+
+ Simulation failed with error: + {{ result.error_message }} +
+
Results
{ - return (await apiClient.get("cities")).data; + return (await apiClient.get("cities")).data.results; } export async function getCityDatasets(cityId: number): Promise { - return (await apiClient.get(`datasets?city=${cityId}`)).data; + return (await apiClient.get(`datasets?city=${cityId}`)).data.results; } export async function getCityCharts(cityId: number): Promise { - return (await apiClient.get(`charts?city=${cityId}`)).data; + return (await apiClient.get(`charts?city=${cityId}`)).data.results; } export async function getCitySimulations( From 1cd52b8cdcc5bfd926b3bc6ea54dd73645f3053f Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 20 Sep 2023 22:12:51 -0400 Subject: [PATCH 23/25] Add City reference on Simulation Results --- .../migrations/0008_simulation_results.py | 13 ++++++++-- uvdat/core/models.py | 2 ++ uvdat/core/tasks/simulations.py | 9 ++++--- uvdat/core/views.py | 19 ++++++++------- uvdat/settings.py | 1 - web/src/api/rest.ts | 24 +++++++++++++++---- web/src/components/SimulationsPanel.vue | 19 +++++++++------ 7 files changed, 60 insertions(+), 27 deletions(-) diff --git a/uvdat/core/migrations/0008_simulation_results.py b/uvdat/core/migrations/0008_simulation_results.py index 61b52833..a67661bf 100644 --- a/uvdat/core/migrations/0008_simulation_results.py +++ b/uvdat/core/migrations/0008_simulation_results.py @@ -1,6 +1,7 @@ -# Generated by Django 4.1 on 2023-09-21 00:24 +# Generated by Django 4.1 on 2023-09-21 01:38 from django.db import migrations, models +import django.db.models.deletion import django_extensions.db.fields @@ -35,9 +36,17 @@ class Migration(migrations.Migration): ('input_args', models.JSONField(blank=True, null=True)), ('output_data', models.JSONField(blank=True, null=True)), ('error_message', models.TextField(blank=True, null=True)), + ( + 'city', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='simulation_results', + to='core.city', + ), + ), ], options={ - 'unique_together': {('simulation_id', 'input_args')}, + 'unique_together': {('simulation_id', 'city', 'input_args')}, }, ), ] diff --git a/uvdat/core/models.py b/uvdat/core/models.py index 27911708..5bc8e658 100644 --- a/uvdat/core/models.py +++ b/uvdat/core/models.py @@ -69,6 +69,7 @@ class Chart(models.Model): class SimulationResult(TimeStampedModel): simulation_id = models.IntegerField() + city = models.ForeignKey(City, on_delete=models.CASCADE, related_name='simulation_results') input_args = models.JSONField(blank=True, null=True) output_data = models.JSONField(blank=True, null=True) error_message = models.TextField(null=True, blank=True) @@ -76,5 +77,6 @@ class SimulationResult(TimeStampedModel): class Meta: unique_together = ( 'simulation_id', + 'city', 'input_args', ) diff --git a/uvdat/core/tasks/simulations.py b/uvdat/core/tasks/simulations.py index 3b18730d..0eea2743 100644 --- a/uvdat/core/tasks/simulations.py +++ b/uvdat/core/tasks/simulations.py @@ -8,7 +8,7 @@ import large_image import shapely -from uvdat.core.models import Dataset, SimulationResult +from uvdat.core.models import City, Dataset, SimulationResult from uvdat.core.serializers import DatasetSerializer, SimulationResultSerializer @@ -135,11 +135,14 @@ def get_available_simulations(city_id: int): return sims -def run_simulation(simulation_id: int, **kwargs): +def run_simulation(simulation_id: int, city_id: int, **kwargs): + city = City.objects.get(id=city_id) simulation_matches = [s for s in AVAILABLE_SIMULATIONS if s['id'] == simulation_id] if len(simulation_matches) > 0: sim_result, created = SimulationResult.objects.get_or_create( - simulation_id=simulation_id, input_args=kwargs + simulation_id=simulation_id, + input_args=kwargs, + city=city, ) sim_result.output_data = None sim_result.save() diff --git a/uvdat/core/views.py b/uvdat/core/views.py index 32c06b63..b466b67c 100644 --- a/uvdat/core/views.py +++ b/uvdat/core/views.py @@ -186,10 +186,9 @@ class SimulationViewSet(GenericViewSet): @action( detail=False, methods=['get'], - url_path="available", + url_path=r'available/city/(?P[\d*]+)', ) - def list_available(self, request, **kwargs): - city_id = request.query_params.get('city') + def list_available(self, request, city_id: int, **kwargs): sims = get_available_simulations(city_id) return HttpResponse( json.dumps(sims), @@ -199,14 +198,16 @@ def list_available(self, request, **kwargs): @action( detail=False, methods=['get'], - url_path=r'(?P[\d*]+)/results', + url_path=r'(?P[\d*]+)/city/(?P[\d*]+)/results', ) - def list_results(self, request, simulation_id: int, **kwargs): + def list_results(self, request, simulation_id: int, city_id: int, **kwargs): return HttpResponse( json.dumps( list( SimulationResultSerializer(s).data - for s in SimulationResult.objects.filter(simulation_id=int(simulation_id)).all() + for s in SimulationResult.objects.filter( + simulation_id=int(simulation_id), city__id=city_id + ).all() ) ), status=200, @@ -215,10 +216,10 @@ def list_results(self, request, simulation_id: int, **kwargs): @action( detail=False, methods=['post'], - url_path=r'run/(?P[\d*]+)', + url_path=r'run/(?P[\d*]+)/city/(?P[\d*]+)', ) - def run(self, request, simulation_id: int, **kwargs): - result = run_simulation(int(simulation_id), **request.data) + def run(self, request, simulation_id: int, city_id: int, **kwargs): + result = run_simulation(int(simulation_id), int(city_id), **request.data) return HttpResponse( json.dumps({'result': result}), status=200, diff --git a/uvdat/settings.py b/uvdat/settings.py index 397346ca..8102cffb 100644 --- a/uvdat/settings.py +++ b/uvdat/settings.py @@ -36,7 +36,6 @@ def mutate_configuration(configuration: ComposedConfiguration) -> None: # TODO: configure authentication and remove this workaround configuration.REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = [] configuration.REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] = [] - configuration.REST_FRAMEWORK['APPEND_SLASH'] = False configuration.DATABASES = { 'default': { diff --git a/web/src/api/rest.ts b/web/src/api/rest.ts index e8d5e330..721dbe68 100644 --- a/web/src/api/rest.ts +++ b/web/src/api/rest.ts @@ -23,7 +23,7 @@ export async function getCityCharts(cityId: number): Promise { export async function getCitySimulations( cityId: number ): Promise { - return (await apiClient.get(`simulations/available?city=${cityId}`)).data; + return (await apiClient.get(`simulations/available/city/${cityId}`)).data; } export async function getDataset(datasetId: number): Promise { @@ -69,10 +69,24 @@ export async function clearChart(chartId: number) { await apiClient.post(`charts/${chartId}/clear/`); } -export async function runSimulation(simulationId: number, args: object) { - return (await apiClient.post(`simulations/run/${simulationId}/`, args)).data; +export async function runSimulation( + simulationId: number, + cityId: number, + args: object +) { + return ( + await apiClient.post( + `simulations/run/${simulationId}/city/${cityId}/`, + args + ) + ).data; } -export async function getSimulationResults(simulationId: number) { - return (await apiClient.get(`simulations/${simulationId}/results`)).data; +export async function getSimulationResults( + simulationId: number, + cityId: number +) { + return ( + await apiClient.get(`simulations/${simulationId}/city/${cityId}/results/`) + ).data; } diff --git a/web/src/components/SimulationsPanel.vue b/web/src/components/SimulationsPanel.vue index f86d9e6d..c927fb03 100644 --- a/web/src/components/SimulationsPanel.vue +++ b/web/src/components/SimulationsPanel.vue @@ -19,18 +19,23 @@ export default { function run() { inputForm.value.validate().then(({ valid }) => { if (valid) { - runSimulation(activeSimulation.value.id, selectedInputs.value).then( - ({ result }) => { - tab.value = "old"; - activeResult.value = result.id; - } - ); + runSimulation( + activeSimulation.value.id, + currentCity.value.id, + selectedInputs.value + ).then(({ result }) => { + tab.value = "old"; + activeResult.value = result.id; + }); } }); } function fetchResults() { - getSimulationResults(activeSimulation.value.id).then((results) => { + getSimulationResults( + activeSimulation.value.id, + currentCity.value.id + ).then((results) => { availableResults.value = results; }); } From c1c5661ef927ed498067a207b8ad866cd4a17f0b Mon Sep 17 00:00:00 2001 From: Anne Haley Date: Wed, 20 Sep 2023 22:15:34 -0400 Subject: [PATCH 24/25] Require network mode enabled for network failure animation --- web/src/components/NodeFailureAnimation.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/src/components/NodeFailureAnimation.vue b/web/src/components/NodeFailureAnimation.vue index 15b03438..2eb318bb 100644 --- a/web/src/components/NodeFailureAnimation.vue +++ b/web/src/components/NodeFailureAnimation.vue @@ -2,6 +2,7 @@ import { ref, watch } from "vue"; import { deactivatedNodes } from "@/store"; import { deactivatedNodesUpdated } from "@/utils"; +import { networkVis } from "../store"; export default { props: { @@ -48,6 +49,7 @@ export default { }); return { + networkVis, currentTick, seconds, play, @@ -59,7 +61,10 @@ export default {