288 lines
9.5 KiB
TypeScript
288 lines
9.5 KiB
TypeScript
import { FormEvent, useMemo, useRef, useState } from 'react'
|
|
import {
|
|
NATIONAL_WORKSPACE_PROJECT_NAME,
|
|
NATIONAL_WORKSPACE_REGION,
|
|
REGIONAL_WORKSPACE_PROJECT_NAME,
|
|
} from '../config/primaryFocus'
|
|
import { areasApi } from '../services/api/areas'
|
|
import { datasetsApi } from '../services/api/datasets'
|
|
import { projectsApi } from '../services/api/projects'
|
|
import type {
|
|
AreaCreate,
|
|
AreaListResponse,
|
|
AreaRead,
|
|
DatasetCreateResponse,
|
|
DatasetListResponse,
|
|
ProjectCreate,
|
|
ProjectRead,
|
|
} from '../types'
|
|
|
|
const DEMO_PROJECT_NAME = 'GeoIntel Demo - Building QA'
|
|
|
|
function isDemoProject(project: ProjectRead): boolean {
|
|
return project.name === DEMO_PROJECT_NAME
|
|
}
|
|
|
|
function hasMappedAnalysisContext(
|
|
data: { areas: AreaRead[]; datasets: DatasetCreateResponse[] },
|
|
): boolean {
|
|
const hasRasterDataset = data.datasets.some((dataset) => dataset.dataset_type === 'raster')
|
|
const hasVectorDataset = data.datasets.some(
|
|
(dataset) => dataset.dataset_type === 'vector' || dataset.dataset_type === 'geojson',
|
|
)
|
|
return data.areas.length > 0 && hasRasterDataset && hasVectorDataset
|
|
}
|
|
|
|
export function useProjectWorkspace() {
|
|
const [projects, setProjects] = useState<ProjectRead[]>([])
|
|
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
|
|
const [areas, setAreas] = useState<AreaRead[]>([])
|
|
const [datasets, setDatasets] = useState<DatasetCreateResponse[]>([])
|
|
const [loadingProjects, setLoadingProjects] = useState(false)
|
|
const [loadingAreas, setLoadingAreas] = useState(false)
|
|
const [loadingDatasets, setLoadingDatasets] = useState(false)
|
|
const [archivingProjectId, setArchivingProjectId] = useState<string | null>(null)
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
|
const projectDataRequestSequence = useRef(0)
|
|
|
|
const [projectForm, setProjectForm] = useState<ProjectCreate>({
|
|
name: '',
|
|
description: '',
|
|
region: NATIONAL_WORKSPACE_REGION,
|
|
})
|
|
const [areaForm, setAreaForm] = useState({
|
|
name: '',
|
|
geometry: '',
|
|
crs: 'EPSG:4326',
|
|
})
|
|
|
|
const selectedProject = useMemo(
|
|
() => projects.find((project) => project.id === selectedProjectId) ?? null,
|
|
[projects, selectedProjectId],
|
|
)
|
|
|
|
const fetchProjectData = async (projectId: string): Promise<{ areas: AreaRead[]; datasets: DatasetCreateResponse[] }> => {
|
|
const [areaResponse, datasetResponse]: [AreaListResponse, DatasetListResponse] = await Promise.all([
|
|
areasApi.list(projectId),
|
|
datasetsApi.list(projectId),
|
|
])
|
|
return { areas: areaResponse.items, datasets: datasetResponse.items }
|
|
}
|
|
|
|
const pickInitialProjectId = async (items: ProjectRead[], preferredProjectId?: string | null) => {
|
|
if (preferredProjectId && items.some((project) => project.id === preferredProjectId)) {
|
|
return preferredProjectId
|
|
}
|
|
if (selectedProjectId && items.some((project) => project.id === selectedProjectId)) {
|
|
return selectedProjectId
|
|
}
|
|
const nationalProject = items.find((project) => project.name === NATIONAL_WORKSPACE_PROJECT_NAME)
|
|
if (nationalProject) {
|
|
return nationalProject.id
|
|
}
|
|
const regionalProject = items.find((project) => project.name === REGIONAL_WORKSPACE_PROJECT_NAME)
|
|
if (regionalProject) {
|
|
try {
|
|
const data = await fetchProjectData(regionalProject.id)
|
|
if (data.areas.length > 0 && data.datasets.some((dataset) => dataset.status === 'ready')) {
|
|
return regionalProject.id
|
|
}
|
|
} catch {
|
|
// Continue with the municipality and completeness fallbacks when regional data is unavailable.
|
|
}
|
|
}
|
|
const demoProjects = items.filter(isDemoProject)
|
|
const candidates = Array.from(
|
|
new Map(
|
|
[...items, ...demoProjects].map((project) => [project.id, project]),
|
|
).values(),
|
|
).slice(0, 12)
|
|
const inspectedCandidates: Array<{
|
|
project: ProjectRead
|
|
data: { areas: AreaRead[]; datasets: DatasetCreateResponse[] }
|
|
}> = []
|
|
for (const project of candidates) {
|
|
try {
|
|
const data = await fetchProjectData(project.id)
|
|
inspectedCandidates.push({ project, data })
|
|
if (hasMappedAnalysisContext(data)) {
|
|
return project.id
|
|
}
|
|
} catch {
|
|
// Project list should still render if a candidate's detail endpoints are temporarily unavailable.
|
|
}
|
|
}
|
|
const completeContext = inspectedCandidates.find(
|
|
({ data }) => data.areas.length > 0 && data.datasets.length > 0,
|
|
)
|
|
return completeContext?.project.id ?? demoProjects[0]?.id ?? items[0]?.id ?? null
|
|
}
|
|
|
|
const loadProjects = async (preferredProjectId?: string | null) => {
|
|
setLoadingProjects(true)
|
|
setErrorMessage(null)
|
|
try {
|
|
const [response, nationalResponse, canonicalResponse] = await Promise.all([
|
|
projectsApi.list(),
|
|
projectsApi.list({ name: NATIONAL_WORKSPACE_PROJECT_NAME, limit: 1 }),
|
|
projectsApi.list({ name: REGIONAL_WORKSPACE_PROJECT_NAME, limit: 1 }),
|
|
])
|
|
const regionalItems = [...canonicalResponse.items, ...response.items]
|
|
const items = Array.from(
|
|
new Map(
|
|
[...nationalResponse.items, ...regionalItems].map((project) => [project.id, project]),
|
|
).values(),
|
|
)
|
|
setProjects(items)
|
|
const nextProjectId = await pickInitialProjectId(items, preferredProjectId)
|
|
if (nextProjectId && nextProjectId !== selectedProjectId) {
|
|
setSelectedProjectId(nextProjectId)
|
|
}
|
|
} catch (error) {
|
|
setErrorMessage(error instanceof Error ? error.message : 'Werkruimtes konden niet worden geladen')
|
|
} finally {
|
|
setLoadingProjects(false)
|
|
}
|
|
}
|
|
|
|
const loadProjectData = async (projectId: string) => {
|
|
const requestId = ++projectDataRequestSequence.current
|
|
setLoadingAreas(true)
|
|
setLoadingDatasets(true)
|
|
setErrorMessage(null)
|
|
try {
|
|
const projectData = await fetchProjectData(projectId)
|
|
if (requestId !== projectDataRequestSequence.current) {
|
|
return null
|
|
}
|
|
setAreas(projectData.areas)
|
|
setDatasets(projectData.datasets)
|
|
return projectData
|
|
} catch (error) {
|
|
if (requestId === projectDataRequestSequence.current) {
|
|
setErrorMessage(error instanceof Error ? error.message : 'De gegevens van de werkruimte konden niet worden geladen')
|
|
}
|
|
return null
|
|
} finally {
|
|
if (requestId === projectDataRequestSequence.current) {
|
|
setLoadingAreas(false)
|
|
setLoadingDatasets(false)
|
|
}
|
|
}
|
|
}
|
|
|
|
const createProject = async (event: FormEvent) => {
|
|
event.preventDefault()
|
|
if (!projectForm.name.trim()) {
|
|
setErrorMessage('Geef een naam voor de werkruimte op')
|
|
return
|
|
}
|
|
try {
|
|
const createdProject = await projectsApi.create({
|
|
name: projectForm.name.trim(),
|
|
description: projectForm.description?.trim() || undefined,
|
|
region: projectForm.region?.trim() || NATIONAL_WORKSPACE_REGION,
|
|
})
|
|
setProjectForm((previous) => ({ ...previous, name: '', description: '' }))
|
|
setSelectedProjectId(createdProject.id)
|
|
await loadProjects(createdProject.id)
|
|
} catch (error) {
|
|
setErrorMessage(error instanceof Error ? error.message : 'De werkruimte kon niet worden aangemaakt')
|
|
}
|
|
}
|
|
|
|
const createArea = async (event: FormEvent) => {
|
|
event.preventDefault()
|
|
if (!selectedProjectId) {
|
|
setErrorMessage('Kies eerst een werkruimte')
|
|
return
|
|
}
|
|
let geometry: AreaCreate['geometry']
|
|
try {
|
|
geometry = JSON.parse(areaForm.geometry) as AreaCreate['geometry']
|
|
} catch {
|
|
setErrorMessage('De opgegeven GeoJSON-geometrie is ongeldig')
|
|
return
|
|
}
|
|
try {
|
|
await areasApi.create(selectedProjectId, {
|
|
name: areaForm.name,
|
|
crs: areaForm.crs,
|
|
geometry,
|
|
})
|
|
await loadProjectData(selectedProjectId)
|
|
setAreaForm((previous) => ({ ...previous, name: '' }))
|
|
} catch (error) {
|
|
setErrorMessage(error instanceof Error ? error.message : 'Het gebied kon niet worden aangemaakt')
|
|
}
|
|
}
|
|
|
|
const activateMunicipality = async (niscode: string): Promise<AreaRead | null> => {
|
|
if (!selectedProjectId) {
|
|
setErrorMessage('Kies eerst een werkruimte')
|
|
return null
|
|
}
|
|
setLoadingAreas(true)
|
|
setErrorMessage(null)
|
|
try {
|
|
const area = await areasApi.activateMunicipality(selectedProjectId, niscode)
|
|
await loadProjectData(selectedProjectId)
|
|
return area
|
|
} catch (error) {
|
|
setErrorMessage(error instanceof Error ? error.message : 'De gemeente kon niet als werkgebied worden geladen')
|
|
return null
|
|
} finally {
|
|
setLoadingAreas(false)
|
|
}
|
|
}
|
|
|
|
const archiveProject = async (projectId: string) => {
|
|
setArchivingProjectId(projectId)
|
|
setErrorMessage(null)
|
|
try {
|
|
await projectsApi.update(projectId, { status: 'archived' })
|
|
if (selectedProjectId === projectId) {
|
|
setSelectedProjectId(null)
|
|
resetProjectData()
|
|
}
|
|
await loadProjects()
|
|
} catch (error) {
|
|
setErrorMessage(error instanceof Error ? error.message : 'De werkruimte kon niet worden gearchiveerd')
|
|
} finally {
|
|
setArchivingProjectId(null)
|
|
}
|
|
}
|
|
|
|
const resetProjectData = () => {
|
|
projectDataRequestSequence.current += 1
|
|
setAreas([])
|
|
setDatasets([])
|
|
}
|
|
|
|
return {
|
|
projects,
|
|
selectedProject,
|
|
selectedProjectId,
|
|
areas,
|
|
datasets,
|
|
loadingProjects,
|
|
loadingAreas,
|
|
loadingDatasets,
|
|
archivingProjectId,
|
|
errorMessage,
|
|
projectForm,
|
|
areaForm,
|
|
loadProjects,
|
|
loadProjectData,
|
|
createProject,
|
|
createArea,
|
|
activateMunicipality,
|
|
archiveProject,
|
|
resetProjectData,
|
|
setSelectedProjectId,
|
|
setErrorMessage,
|
|
setProjectForm,
|
|
setAreaForm,
|
|
}
|
|
}
|