Skip to content

Commit

Permalink
Merge pull request #35 from CUAHSI/develop
Browse files Browse the repository at this point in the history
Merging latest HydroProcessDB changes
  • Loading branch information
devincowan authored Dec 9, 2024
2 parents 1f2c2ba + 043a6a1 commit b80ed52
Show file tree
Hide file tree
Showing 16 changed files with 389 additions and 108 deletions.
8 changes: 7 additions & 1 deletion api/hydroprocess_db/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

from geoalchemy2 import Geometry, WKBElement, shape
from geojson_pydantic import Feature, FeatureCollection, Point
from pydantic import ConfigDict, model_serializer
from pydantic import ConfigDict, model_serializer, BaseModel
from pydantic_extra_types.coordinate import Latitude, Longitude
from shapely import to_geojson
from sqlmodel import Column, Field, Relationship, SQLModel
from typing import List, Optional


class Citation(SQLModel, table=True):
Expand Down Expand Up @@ -227,3 +228,8 @@ class ProcessAltName(SQLModel, table=True):
process_id: int | None = Field(default=None)

process_taxonomy: ProcessTaxonomy | None = Relationship(back_populates="process_alt_name")

class ModelCountRequest(BaseModel):
spatialzone_ids: Optional[List[int]] = None
temporalzone_ids: Optional[List[int]] = None
process_taxonomy_ids: Optional[List[int]] = None
38 changes: 23 additions & 15 deletions api/hydroprocess_db/app/routers/statistics/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,39 @@
from sqlmodel import select

from app.db import get_session
from app.models import ModelType, PerceptualModel
from app.models import ModelType, PerceptualModel, ModelCountRequest, ProcessTaxonomy

router = APIRouter()


@router.get(
@router.post(
"/model_type_count",
description="Get the count of models for each model type.",
response_model=dict[str, int],
)
def get_model_count_by_type(*, session=Depends(get_session)):
"""
Get the count of models for each model type.
Parameters:
- session: The async session to use for database operations.
Returns:
- A dictionary with the count of models for each model type.
"""
def get_model_count_by_type(
request: ModelCountRequest,
session=Depends(get_session)
):
model_types = session.exec(select(ModelType)).all()
model_type_count = {}

for model_type in model_types:
matching_models = session.query(PerceptualModel).where(PerceptualModel.model_type_id == model_type.id)
model_type_count[model_type.name] = matching_models.count()
query = session.query(PerceptualModel).where(PerceptualModel.model_type_id == model_type.id)

if request.spatialzone_ids:
query = query.where(PerceptualModel.spatialzone_id.in_(request.spatialzone_ids))

if request.temporalzone_ids:
query = query.where(PerceptualModel.temporalzone_id.in_(request.temporalzone_ids))

if request.process_taxonomy_ids:
query = query.join(PerceptualModel.process_taxonomies).where(
ProcessTaxonomy.id.in_(request.process_taxonomy_ids)
)

matching_models = query.all()
model_type_count[model_type.name] = len(matching_models)

return model_type_count


Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"vite-plugin-vuetify": "^1.0.2",
"vue": "^3.3.4",
"vue-router": "^4.2.4",
"vuetify": "^3.3.21"
"vuetify": "^3.7.4"
},
"devDependencies": {
"@mdi/font": "^7.3.67",
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ const paths = [
attrs: { to: "/" },
label: "Map",
},
{
attrs: { to: "/api" },
label: "API",
},
// {
// attrs: { to: "/api" },
// label: "API",
// },
{
attrs: { to: "/about" },
label: "About",
Expand Down
Binary file added frontend/src/assets/hilary.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/nsf_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/ryoko.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/sdsu_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 16 additions & 2 deletions frontend/src/components/DataViewDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,29 @@ let querying = ref(true)
let modelTypeCounts = ref({})
let totalModels = ref(0)
const query = async () => {
const query = async (filters = {}) => {
querying.value = true
const response = await fetch(ENDPOINTS.model_type_count)
const response = await fetch(ENDPOINTS.model_type_count, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(filters)
});
const counts = await response.json()
// Delete the 'Figure model (Hand-drawn)' key
delete counts['Figure model (Hand-drawn)']
modelTypeCounts.value = counts
totalModels.value = Object.values(counts).reduce((acc, count) => acc + count, 0)
querying.value = false
}
defineExpose({
query
})
query()
</script>
Expand Down
121 changes: 115 additions & 6 deletions frontend/src/components/FilterDrawer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,37 @@
<v-sheet class="mx-auto" elevation="8">
<h3 class="text-h6 ma-2 text-center">Model Filters</h3>
<v-divider></v-divider>
<v-autocomplete v-model="selectedProcesses" :items="process_taxonomies" item-title="process" item-value="id"
<!-- <v-autocomplete v-model="selectedProcesses" :items="process_taxonomies" item-title="process" item-value="id"
label="Process Taxonomies" @update:modelValue="filter" clearable chips multiple
:loading="filtering"></v-autocomplete>
:loading="filtering"></v-autocomplete> -->
<v-text-field
v-model="searchTreeText"
label="Search Process Taxonomies"
:clear-icon="mdiCloseCircleOutline"
clearable
dark
flat
hide-details
solo-inverted>
</v-text-field>
<v-treeview
v-model:selected="selectedTreeItems"
:items="treeViewData"
select-strategy="clasic"
item-value="id"
selectable
:search="searchTreeText"
activatable
@update:modelValue="updateMap"
>
<template v-slot:prepend="{ item, isOpen }">
<v-icon>
{{ isOpen ? mdiFolderOpen : mdiFolder }}
</v-icon>
</template>
</v-treeview>


<v-autocomplete v-model="selectedSpatialZones" :items="spatialZones" item-title="spatial_property" item-value="id"
label="Spatial Zones" @update:modelValue="filter" clearable chips multiple :loading="filtering"></v-autocomplete>
<v-autocomplete v-model="selectedTemporalZones" :items="temporalZones" item-title="temporal_property"
Expand All @@ -15,7 +43,7 @@
<v-card-text>
<v-btn-toggle v-model="textSearchFields" @update:modelValue="filter" class="mb-2" multiple outlined
variant="text" divided>
<v-btn value="long_name">Name</v-btn>
<v-btn value="long_name">Title</v-btn>
<v-btn value="citation">Citation</v-btn>
<v-btn value="textmodel_snipped">Abstract</v-btn>
</v-btn-toggle>
Expand All @@ -31,11 +59,14 @@
import { ref, computed, nextTick } from 'vue'
import { usePerceptualModelStore } from "@/stores/perceptual_models";
import { useMapStore } from '@/stores/map';
import { mdiFolderOpen, mdiFolder, mdiCloseCircleOutline } from '@mdi/js';
const perceptualModelStore = usePerceptualModelStore();
const mapStore = useMapStore()
defineEmits(['selectModel', 'toggle'])
const emit = defineEmits(['selectModel', 'toggle', 'onFilter'])
let modelFeatures = ref({})
const filtering = ref()
Expand All @@ -53,22 +84,90 @@ const temporalZones = ref([])
const selectedTemporalZones = ref([])
const searchTerm = ref(null)
const textSearchFields = ref([])
const treeViewData = ref([])
const selectedTreeItems = ref([])
const searchTreeText = ref('')
const hasTextSearchFields = computed(() => {
return textSearchFields.value.length > 0
})
// Fetch the process taxonomies, spatial zones, and temporal zones
perceptualModelStore.fetchProcessTaxonomies().then((pt) => {
process_taxonomies.value = pt
process_taxonomies.value = pt;
treeViewData.value = buildTree(pt);
})
function buildTree(data) {
const root = {};
// Helper function to insert item into the correct place in the tree
const insert = (path, item) => {
let current = root;
path.forEach((part, index) => {
// Check if part already exists as a child, if not create it
if (!current[part]) {
current[part] = {
title: part,
id: item.id,
children: {}
};
}
// If it's the last part, assign the item values to the node
if (index === path.length - 1) {
current[part] = {
id: item.id,
title: item.process,
children: current[part].children || {}
};
}
current = current[part].children;
});
};
// Insert each item in data into the tree
data.forEach(item => {
const path = item.identifier.split(".");
insert(path, item);
});
// Convert tree object with nested children into desired array format
const convertToArray = (node) => {
return Object.values(node).map(child => {
const childrenArray = convertToArray(child.children);
const nodeObject = {
id: child.id,
title: child.title
};
if (childrenArray.length > 0) {
nodeObject.children = childrenArray;
}
return nodeObject;
});
};
return convertToArray(root);
}
perceptualModelStore.fetchSpatialZones().then((sz) => {
replaceNwithNone(sz, 'spatial_property');
spatialZones.value = sz
})
perceptualModelStore.fetchTemporalZones().then((tz) => {
replaceNwithNone(tz, 'temporal_property');
temporalZones.value = tz
})
const replaceNwithNone = (items, propName) => {
for(let item of items){
if(item[propName] === 'N') {
item[propName] = "None";
break;
}
}
return items;
}
const checkSearchTerm = (searchTerm, fieldsToSearch, feature) => {
if (!searchTerm) {
return true
Expand All @@ -83,6 +182,8 @@ const checkSearchTerm = (searchTerm, fieldsToSearch, feature) => {
async function filter() {
emit('onFilter', {selectedSpatialZones, selectedTemporalZones, selectedProcesses})
filtering.value = true
await nextTick()
// reset search term if no text search fields are selected
Expand All @@ -94,12 +195,20 @@ async function filter() {
const spatial = selectedSpatialZones.value.length == 0 || selectedSpatialZones.value.includes(feature.properties.spatialzone_id)
const temporal = selectedTemporalZones.value.length == 0 || selectedTemporalZones.value.includes(feature.properties.temporalzone_id)
const search = checkSearchTerm(searchTerm.value, textSearchFields.value, feature)
return process && spatial && temporal && search
}
mapStore.filterFeatures(filterFunction)
filtering.value = false
}
const updateMap = async () => {
selectedProcesses.value = [];
selectedTreeItems.value.forEach((item) => {
selectedProcesses.value.push(item);
});
await nextTick();
filter();
}
</script>
<style scoped>
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/components/TheAppBar.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<v-app-bar v-if="!$route.meta.hideNavigation" color="navbar" ref="appBar" id="app-bar" elevate-on-scroll fixed app>
<div class="d-flex align-end full-height pa-2 align-center w-100">
<div class="d-flex align-end full-height pa-2 align-center w-100 position-relative">
<v-app-bar-title>Perceptual Models Around the World<div class="text-subtitle-1">McMillan Hydrology Lab</div>
</v-app-bar-title>

Expand All @@ -16,16 +16,16 @@
</nav>
</v-card>
<v-spacer></v-spacer>
<UserLogin @logged-in="login" v-if="!mdAndDown" :mobile="false" />
<!-- <UserLogin @logged-in="login" v-if="!mdAndDown" :mobile="false" /> -->

<v-app-bar-nav-icon @click="$emit('toggleMobileNav')" v-else />
<!-- <v-app-bar-nav-icon @click="$emit('toggleMobileNav')" v-else /> -->
</div>
</v-app-bar>
</template>
<script setup>
import { RouterLink } from 'vue-router'
// import { RouterLink } from 'vue-router'
import { useDisplay } from 'vuetify'
import UserLogin from "@/components/UserLogin.vue";
// import UserLogin from "@/components/UserLogin.vue";
import { useAuthStore } from '../stores/auth';
defineProps(['paths'])
defineEmits(['toggleMobileNav'])
Expand All @@ -51,6 +51,8 @@ function login(){
.nav-items {
border-radius: 2rem !important;
overflow: hidden;
position: absolute;
left: 43%;
&>a.v-btn:first-child {
border-top-left-radius: 2rem !important;
Expand Down
Loading

0 comments on commit b80ed52

Please sign in to comment.