diff --git a/app/assets/stylesheets/masks.scss b/app/assets/stylesheets/masks.scss new file mode 100644 index 00000000..87e80863 --- /dev/null +++ b/app/assets/stylesheets/masks.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the masks controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: https://sass-lang.com/ diff --git a/app/controllers/masks_controller.rb b/app/controllers/masks_controller.rb new file mode 100644 index 00000000..bbbbad9e --- /dev/null +++ b/app/controllers/masks_controller.rb @@ -0,0 +1,24 @@ +class MasksController < ApplicationController + + def create + authorize! + @mask = Mask.new(:name => params[:name], :file => params[:file]) + if @mask.save + render json: @mask, status: :created + else + render json: @mask.errors, status: :unprocessable_entity + end + end + + def show + authorize! + @mask = Mask.find_by(:name => params[:name]) + redirect_to @mask.file + end + + def index + authorize! + @masks = Mask.all + render json: @masks + end +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 80733921..51559764 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -25,7 +25,7 @@ def edit def create - @project = @team.projects.new(params.require(:project).permit(:name, :extent)) + @project = @team.projects.new(params.require(:project).permit(:name, :extent, :cql, :layer)) if @project.save redirect_to team_projects_url(@team) else @@ -48,7 +48,10 @@ def update @team = @project.team existing_source = @project.source || {} existing_source["name"] = params.require(:project).require(:name) - existing_source["extent"] = params.require(:project).require(:extent).split(",").map(&:to_f) + existing_source["extent"] = params.require(:project).require(:extent).split(",").map(&:to_f) + existing_source["cql"] = params.require(:project).require(:cql) + existing_source["layer"] = params.require(:project).require(:layer) + if @project.update(source: existing_source) if params[:commit] == 'Save and open project' redirect_to project_url(@project) diff --git a/app/helpers/masks_helper.rb b/app/helpers/masks_helper.rb new file mode 100644 index 00000000..fe272cad --- /dev/null +++ b/app/helpers/masks_helper.rb @@ -0,0 +1,2 @@ +module MasksHelper +end diff --git a/app/javascript/modelling/worker/generateDistanceMap.js b/app/javascript/modelling/worker/generateDistanceMap.js index f4e35321..be581089 100644 --- a/app/javascript/modelling/worker/generateDistanceMap.js +++ b/app/javascript/modelling/worker/generateDistanceMap.js @@ -2,7 +2,7 @@ import { kdTree } from 'kd-tree-javascript' import { NumericTileGrid } from "../../projects/modelling/tile_grid" import { getMedianCellSize } from "../../projects/modelling/components/cell_area_component" -export function generateDistanceMap(input) { +export function generateDistanceMap(input, mask = null) { const result = new NumericTileGrid( input.zoom, input.x, input.y, input.width, input.height @@ -35,7 +35,7 @@ export function generateDistanceMap(input) { for (let x = result.x; x < result.x + result.width; ++x) { for (let y = result.y; y < result.y + result.height; ++y) { const [point, distance] = tree.nearest({ x, y }, 1)[0] - result.set(x, y, distance * tileSize) + result.set(x, y, !mask ? (distance * tileSize) : (mask.get(x, y) ? (distance * tileSize) : NaN)) } } diff --git a/app/javascript/projects/model_view.tsx b/app/javascript/projects/model_view.tsx index b8d00ef0..9d784b47 100644 --- a/app/javascript/projects/model_view.tsx +++ b/app/javascript/projects/model_view.tsx @@ -37,8 +37,11 @@ export interface ModelViewProps { getDatasets: getDatasets extent: Extent zoom: number + mask: boolean + maskLayer: string + maskCQL: string } -export function ModelView({ visible, initialTransform, setTransform, initialModel, setModel, createOutputLayer, deleteOutputLayer, saveMapLayer, setProcessing, autoProcessing, process, setProcess, saveModel, getDatasets, extent, zoom }: ModelViewProps) { +export function ModelView({ visible, initialTransform, setTransform, initialModel, setModel, createOutputLayer, deleteOutputLayer, saveMapLayer, setProcessing, autoProcessing, process, setProcess, saveModel, getDatasets, extent, zoom, mask, maskLayer, maskCQL }: ModelViewProps) { const ref = React.useRef(null) const [editor, setEditor] = React.useState() const [engine, setEngine] = React.useState() @@ -65,7 +68,7 @@ export function ModelView({ visible, initialTransform, setTransform, initialMode }) const engine = new Engine("landscapes@1.0.0") - createDefaultComponents(saveMapLayer, saveModel, getDatasets, extent, zoom).forEach(component => { + createDefaultComponents(saveMapLayer, saveModel, getDatasets, extent, zoom, mask, maskLayer, maskCQL).forEach(component => { editor.register(component) engine.register(component) }) diff --git a/app/javascript/projects/modelling/bounding_box.ts b/app/javascript/projects/modelling/bounding_box.ts index 54ec8ab4..0be871f3 100644 --- a/app/javascript/projects/modelling/bounding_box.ts +++ b/app/javascript/projects/modelling/bounding_box.ts @@ -4,6 +4,9 @@ import { Extent, getArea } from "ol/extent" import { createXYZ } from "ol/tilegrid" import * as proj4 from "proj4" +import { BooleanTileGrid, TileGridJSON, fromJSON } from "./tile_grid" +import { GeoJSON } from "ol/format" +import { Tile } from "ol" const westHorsely = [-49469.089243, 6669018.450996] const bexhill = [55641.379277, 6570068.329224] @@ -69,4 +72,143 @@ export function WKTfromExtent(extent: Extent): string { // Required format for some requests export function bboxFromExtent(extent: Extent): string { return `${extent.join(",")},EPSG:3857` -} \ No newline at end of file +} + +const maskMap = new Map() + +export async function maskFromExtentAndShape(extent: Extent, zoom: number, shapeLayer: string, shapeId: string, maskMode: boolean = false): Promise { + const id = `${shapeLayer}${shapeId}` + if(maskMap.has(id)) return maskMap.get(id) as BooleanTileGrid + + else{ + + const cachedMask = await loadMask(id) + console.log(cachedMask) + if(cachedMask !== null) { + maskMap.set(id, cachedMask) + return cachedMask + }else{ + + const tileGrid = createXYZ() + const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom) + + let mask = new BooleanTileGrid( + zoom, + outputTileRange.minX, + outputTileRange.minY, + outputTileRange.getWidth(), + outputTileRange.getHeight(), + !maskMode + ) + + if(!maskMode) return mask + else{ + const response = await fetch( + "https://landscapes.wearepal.ai/geoserver/wfs?" + + new URLSearchParams( + { + outputFormat: 'application/json', + request: 'GetFeature', + typeName: shapeLayer, + srsName: 'EPSG:3857', + CQL_FILTER: shapeId + } + ) + ) + + const features = new GeoJSON().readFeatures(await response.json()) + + const len = mask.width * mask.height + const seg = Math.ceil(len / 20) + + + for (let feature of features) { + const geom = feature.getGeometry() + if (geom === undefined) { continue } + + const featureTileRange = tileGrid.getTileRangeForExtentAndZ( + geom.getExtent(), + zoom + ) + + let i = 0 + + for ( + let x = Math.max(featureTileRange.minX, outputTileRange.minX); + x <= Math.min(featureTileRange.maxX, outputTileRange.maxX); + ++x + ) { + for ( + let y = Math.max(featureTileRange.minY, outputTileRange.minY); + y <= Math.min(featureTileRange.maxY, outputTileRange.maxY); + ++y + ) { + + // DEBUG, shows progress percentage in console + if (i % seg === 0) { + console.log(Math.floor((i / len) * 100) + "%") + } + + const center = tileGrid.getTileCoordCenter([zoom, x, y]) + if (geom.intersectsCoordinate(center)) { + mask.set(x, y, true) + } + + i++ + } + } + } + + maskMap.set(id, mask) + saveMask(mask, id) + + return mask + } + } + + + } +} + +function saveMask(mask: BooleanTileGrid, id: string){ + id = id.replace(/'/g, "_") + const json = mask.toJSON() + const formData = new FormData() + const blob = new Blob([JSON.stringify(json)], { type: "application/json" }) + formData.append('file', blob, 'mask.json') + formData.append('name', id) + const request = new XMLHttpRequest() + request.open('POST', `/masks`) + request.setRequestHeader('X-CSRF-Token', (document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement).content) + request.send(formData) +} + +function loadMask(id: string): Promise { + id = id.replace(/'/g, "_") + return new Promise((resolve, reject) => { + const request = new XMLHttpRequest() + request.open('GET', `/masks?name=${id}`) + + const csrfTokenElement = document.querySelector('meta[name="csrf-token"]') as HTMLMetaElement + if (csrfTokenElement) { + request.setRequestHeader('X-CSRF-Token', csrfTokenElement.content) + } + + request.onreadystatechange = () => { + if (request.readyState === XMLHttpRequest.DONE) { + if (request.status === 200) { + try { + const response = JSON.parse(request.responseText) + resolve(fromJSON(response as TileGridJSON) as BooleanTileGrid) + } catch (error) { + reject(error) + } + } else { + resolve(null) + } + } + }; + + request.send() + }); +} diff --git a/app/javascript/projects/modelling/components/ati_component.ts b/app/javascript/projects/modelling/components/ati_component.ts index 2a281d25..b0d2434f 100644 --- a/app/javascript/projects/modelling/components/ati_component.ts +++ b/app/javascript/projects/modelling/components/ati_component.ts @@ -5,7 +5,7 @@ import { booleanDataSocket, categoricalDataSocket } from "../socket_types" import { BooleanTileGrid, CategoricalTileGrid } from "../tile_grid" import { BaseComponent } from "./base_component" import { Extent } from "ol/extent" -import { bboxFromExtent } from "../bounding_box" +import { bboxFromExtent, maskFromExtentAndShape } from "../bounding_box" import { GeoJSON } from "ol/format"; import { find } from "lodash" @@ -54,7 +54,7 @@ const trees: TreeType[] = [ ] -async function renderCategoricalData(extent: Extent, zoom: number) : Promise{ +async function renderCategoricalData(extent: Extent, zoom: number, maskLayer: string, maskCQL: string, maskMode: boolean) : Promise{ const tileGrid = createXYZ() const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom) @@ -71,6 +71,8 @@ async function renderCategoricalData(extent: Extent, zoom: number) : Promise n.id === node.id) if (editorNode === undefined) { return } + + const mask = await maskFromExtentAndShape(this.projectExtent, this.projectZoom, this.maskLayer, this.maskCQL, this.maskMode) + const speciesFamilyId = node.data.speciesFamilyId || 1 const tileGrid = createXYZ() @@ -163,7 +172,7 @@ export class BiodiversityComponent extends BaseComponent { const v = isNaN(result.get(featureTileRange.maxX, featureTileRange.minY)) ? 1 : result.get(featureTileRange.maxX, featureTileRange.minY) + 1 // Conversion from EPSG:4326 to EPSG:3857 is not perfect, so we need to check if the point is within the tile range - if(toIndex(result, featureTileRange.maxX, featureTileRange.minY) !== undefined) { + if(toIndex(result, featureTileRange.maxX, featureTileRange.minY) !== undefined && mask.get(featureTileRange.maxX, featureTileRange.minY) === true){ result.set(featureTileRange.maxX, featureTileRange.minY, v) } diff --git a/app/javascript/projects/modelling/components/census_component.ts b/app/javascript/projects/modelling/components/census_component.ts index a57eb688..d6a70acc 100644 --- a/app/javascript/projects/modelling/components/census_component.ts +++ b/app/javascript/projects/modelling/components/census_component.ts @@ -9,7 +9,7 @@ import { SelectControl } from "../controls/select" import { Feature } from "ol" import { Geometry } from "ol/geom" import { Extent } from "ol/extent" -import { bboxFromExtent } from "../bounding_box" +import { bboxFromExtent, maskFromExtentAndShape } from "../bounding_box" interface CensusDataset { code: string @@ -757,14 +757,20 @@ export class CensusComponent extends BaseComponent { cachedOutput: Map projectZoom: number projectExtent: Extent + maskMode: boolean + maskLayer: string + maskCQL: string - constructor(projectExtent: Extent, projectZoom: number) { + constructor(projectExtent: Extent, projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { super("UK Census 2021") this.category = "Inputs" this.cachedData = undefined this.cachedOutput = new Map() this.projectExtent = projectExtent this.projectZoom = projectZoom + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL } async builder(node: Node) { @@ -820,6 +826,8 @@ export class CensusComponent extends BaseComponent { if (editorNode === undefined) { return } const features = this.cachedData ? this.cachedData : new GeoJSON().readFeatures(await fetchCensusShapefilesFromExtent(bboxFromExtent(this.projectExtent))) + + const mask = await maskFromExtentAndShape(this.projectExtent, this.projectZoom, this.maskLayer, this.maskCQL, this.maskMode) const datasetIndex = node.data.censusDatasetId ? node.data.censusDatasetId as number : 0 const options = censusDatasets[datasetIndex].options @@ -867,7 +875,7 @@ export class CensusComponent extends BaseComponent { ++y ) { const center = tileGrid.getTileCoordCenter([this.projectZoom, x, y]) - if (geom.intersectsCoordinate(center)) { + if (geom.intersectsCoordinate(center) && mask.get(x, y) === true){ result.set(x, y, feature.get(code) / n * 100) } } diff --git a/app/javascript/projects/modelling/components/crome_component.ts b/app/javascript/projects/modelling/components/crome_component.ts index dbb48729..ec0e79b2 100644 --- a/app/javascript/projects/modelling/components/crome_component.ts +++ b/app/javascript/projects/modelling/components/crome_component.ts @@ -6,7 +6,7 @@ import { booleanDataSocket, categoricalDataSocket } from "../socket_types" import GeoJSON from "ol/format/GeoJSON" import { BooleanTileGrid, CategoricalTileGrid } from "../tile_grid" import { Extent } from "ol/extent" -import { bboxFromExtent } from "../bounding_box" +import { bboxFromExtent, maskFromExtentAndShape } from "../bounding_box" import { TileRange } from "ol" import TileGrid from "ol/tilegrid/TileGrid" @@ -123,7 +123,7 @@ async function fetchCROMEFromExtent(bbox: string, source: string, count: number, return await response.json() } -async function loadFeaturesToGrid(grid: CategoricalTileGrid, tileRange: TileRange, tileGrid: TileGrid, features: any, map: Map) : Promise{ +async function loadFeaturesToGrid(grid: CategoricalTileGrid, tileRange: TileRange, tileGrid: TileGrid, features: any, map: Map, mask: BooleanTileGrid) : Promise{ // given a grid and a set of features, load the features into the grid for (let feature of features) { @@ -152,7 +152,7 @@ async function loadFeaturesToGrid(grid: CategoricalTileGrid, tileRange: TileRang ++y ) { const tileExtent = tileGrid.getTileCoordExtent([grid.zoom, x, y]) - if (geom.intersectsExtent(tileExtent)) { + if (geom.intersectsExtent(tileExtent) && mask.get(x, y) === true) { grid.set(x, y, index) } } @@ -161,7 +161,10 @@ async function loadFeaturesToGrid(grid: CategoricalTileGrid, tileRange: TileRang return grid } -async function renderCategoricalData(extent: Extent, zoom: number) { +async function renderCategoricalData(extent: Extent, zoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) : Promise<[CategoricalTileGrid, CategoricalTileGrid]> { + + + const mask = await maskFromExtentAndShape(extent, zoom, maskLayer, maskCQL, maskMode) const tileGrid = createXYZ() const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom) @@ -212,9 +215,9 @@ async function renderCategoricalData(extent: Extent, zoom: number) { if (features.length === 0) break - await loadFeaturesToGrid(result, outputTileRange, tileGrid, features, map) + await loadFeaturesToGrid(result, outputTileRange, tileGrid, features, map, mask) - await loadFeaturesToGrid(resultOG, outputTileRange, tileGrid, features, mapOG) + await loadFeaturesToGrid(resultOG, outputTileRange, tileGrid, features, mapOG, mask) if (features.length < count) break @@ -232,14 +235,20 @@ export class CROMEComponent extends BaseComponent { outputCache: Map projectExtent: Extent projectZoom: number + maskMode: boolean + maskLayer: string + maskCQL: string - constructor(projectExtent: Extent, projectZoom: number) { + constructor(projectExtent: Extent, projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { super("Crop Map of England CROME") this.category = "Inputs" this.categoricalData = null this.outputCache = new Map() this.projectExtent = projectExtent this.projectZoom = projectZoom + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL } async builder(node: Node) { @@ -260,7 +269,7 @@ export class CROMEComponent extends BaseComponent { async worker(node: NodeData, inputs: WorkerInputs, outputs: WorkerOutputs, ...args: unknown[]) { if (this.categoricalData === null) { - this.categoricalData = await renderCategoricalData(this.projectExtent, this.projectZoom) + this.categoricalData = await renderCategoricalData(this.projectExtent, this.projectZoom, this.maskMode, this.maskLayer, this.maskCQL) } const categoricalData = this.categoricalData! diff --git a/app/javascript/projects/modelling/components/dataset_component.ts b/app/javascript/projects/modelling/components/dataset_component.ts index 67d5d67b..352bb60f 100644 --- a/app/javascript/projects/modelling/components/dataset_component.ts +++ b/app/javascript/projects/modelling/components/dataset_component.ts @@ -7,6 +7,7 @@ import { SelectControl } from "../controls/select" import { CompiledDatasetRecord, getDataset } from "../../saved_dataset" import { Extent } from "ol/extent" import { createXYZ } from "ol/tilegrid" +import { maskFromExtentAndShape } from "../bounding_box" async function fetchDataset(datasetId: number, teamId: number) { return new Promise<{ error: { message: string }; out: { model: BooleanTileGrid | NumericTileGrid | CategoricalTileGrid } }>((resolve) => { @@ -23,13 +24,19 @@ export class PrecompiledModelComponent extends BaseComponent { models: CompiledDatasetRecord[] projectExtent: Extent projectZoom: number + maskMode: boolean + maskLayer: string + maskCQL: string - constructor(getDatasets: getDatasets, projectExtent: Extent, projectZoom: number) { + constructor(getDatasets: getDatasets, projectExtent: Extent, projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { super("Load Dataset") this.category = "Inputs" this.modelSource = getDatasets this.projectExtent = projectExtent this.projectZoom = projectZoom + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL } async builder(node: Node) { @@ -85,6 +92,9 @@ export class PrecompiledModelComponent extends BaseComponent { delete editorNode.meta.errorMessage; if (dataset) { + + const mask = await maskFromExtentAndShape(this.projectExtent, this.projectZoom, this.maskLayer, this.maskCQL, this.maskMode) + try { const response = await fetchDataset(dataset.id, dataset.team_id); if (response.error) { @@ -97,13 +107,13 @@ export class PrecompiledModelComponent extends BaseComponent { if (model instanceof BooleanTileGrid) { const out = outputs['out'] = editorNode.meta.output = new BooleanTileGrid(this.projectZoom, outputTileRange.minX, outputTileRange.minY, outputTileRange.getWidth(), outputTileRange.getHeight()) - out.iterate((x, y, v) => out.set(x, y, model.get(x, y, this.projectZoom))) + out.iterate((x, y, v) => out.set(x, y, mask.get(x, y) === true ? model.get(x, y, this.projectZoom) : false)) } else if (model instanceof CategoricalTileGrid) { const out = outputs['out'] = editorNode.meta.output = new CategoricalTileGrid(this.projectZoom, outputTileRange.minX, outputTileRange.minY, outputTileRange.getWidth(), outputTileRange.getHeight(), undefined, model.labels) - out.iterate((x, y, v) => out.set(x, y, model.get(x, y, this.projectZoom))) + out.iterate((x, y, v) => out.set(x, y, mask.get(x, y) === true ? model.get(x, y, this.projectZoom) : 255)) } else if (model instanceof NumericTileGrid) { const out = new NumericTileGrid(this.projectZoom, outputTileRange.minX, outputTileRange.minY, outputTileRange.getWidth(), outputTileRange.getHeight(), NaN) - out.iterate((x, y, v) => out.set(x, y, model.get(x, y, this.projectZoom))) + out.iterate((x, y, v) => out.set(x, y, mask.get(x, y) === true ? model.get(x, y, this.projectZoom) : NaN)) if(out.getMinMax()[1] === -Infinity) { editorNode.meta.errorMessage = "No valid data found in dataset. Possible cause: no coverage for selected area." diff --git a/app/javascript/projects/modelling/components/designations_component.ts b/app/javascript/projects/modelling/components/designations_component.ts index 93aa14ba..b9cc6335 100644 --- a/app/javascript/projects/modelling/components/designations_component.ts +++ b/app/javascript/projects/modelling/components/designations_component.ts @@ -7,8 +7,12 @@ import { BooleanTileGrid } from "../tile_grid" import { designations, Designation } from "../designations" import { retrieveModelData } from "../model_retrieval" import { TypedArray } from "d3" +import { maskFromExtentAndShape } from "../bounding_box" -async function renderDesignation(extent: Extent, zoom: number, designation: Designation, cacheFn: (result : BooleanTileGrid) => BooleanTileGrid) : Promise{ +async function renderDesignation(extent: Extent, zoom: number, designation: Designation, cacheFn: (result : BooleanTileGrid) => BooleanTileGrid, maskMode: boolean, maskLayer: string, maskCQL: string) : Promise{ + + + const mask = await maskFromExtentAndShape(extent, zoom, maskLayer, maskCQL, maskMode) const tileGrid = createXYZ() const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom) @@ -31,7 +35,7 @@ async function renderDesignation(extent: Extent, zoom: number, designation: Desi let x = (outputTileRange.minX + i % image.getWidth()) let y = (outputTileRange.minY + Math.floor(i / image.getWidth())) - result.set(x, y, rasters[3][i] === 0 ? false : true) + result.set(x, y, rasters[3][i] === 0 ? false : (mask.get(x, y) === true ? true : false)) } @@ -42,13 +46,19 @@ export class DesignationsComponent extends BaseComponent { projectExtent: Extent projectZoom: number cachedDesignations: Map + maskMode: boolean + maskLayer: string + maskCQL: string - constructor(projectExtent: Extent, projectZoom: number) { + constructor(projectExtent: Extent, projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { super("Designations") this.category = "Inputs" this.projectExtent = projectExtent this.projectZoom = projectZoom this.cachedDesignations = new Map() + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL } async builder(node: Node) { @@ -63,7 +73,8 @@ export class DesignationsComponent extends BaseComponent { (r : BooleanTileGrid) => { this.cachedDesignations.set(designation.value, r) return r - } + }, + this.maskMode, this.maskLayer, this.maskCQL ) ) } diff --git a/app/javascript/projects/modelling/components/digital_model_component.ts b/app/javascript/projects/modelling/components/digital_model_component.ts index fd678d9f..aea6ab87 100644 --- a/app/javascript/projects/modelling/components/digital_model_component.ts +++ b/app/javascript/projects/modelling/components/digital_model_component.ts @@ -9,6 +9,7 @@ import { createXYZ } from "ol/tilegrid" import { TypedArray } from "d3" import { retrieveModelDataWCS } from "../model_retrieval" import { Extent } from "ol/extent" +import { maskFromExtentAndShape } from "../bounding_box" interface DigitalModel { id: number @@ -39,13 +40,19 @@ export class DigitalModelComponent extends BaseComponent { outputCache: Map projectZoom: number projectExtent: Extent + maskMode: boolean + maskLayer: string + maskCQL: string - constructor(projectExtent: Extent, projectZoom: number) { + constructor(projectExtent: Extent, projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { super("Digital Model") this.category = "Inputs" this.projectExtent = projectExtent this.projectZoom = projectZoom this.outputCache = new Map() + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL } async builder(node: Node) { @@ -80,6 +87,8 @@ export class DigitalModelComponent extends BaseComponent { let index = node.data.sourceId if (index === undefined) { index = 0 } + const mask = await maskFromExtentAndShape(this.projectExtent, this.projectZoom, this.maskLayer, this.maskCQL, this.maskMode) + let digitalModel = ModelList.find(a => a.id == index) if (digitalModel?.source) { @@ -102,7 +111,7 @@ export class DigitalModelComponent extends BaseComponent { let x = (outputTileRange.minX + i % image.getWidth()) let y = (outputTileRange.minY + Math.floor(i / image.getWidth())) - out.set(x, y, (rasters[0][i]) === -32767 ? NaN : (rasters[0][i])) + out.set(x, y, mask.get(x, y) === true ? (rasters[0][i]) === -32767 ? NaN : (rasters[0][i]) : NaN) } diff --git a/app/javascript/projects/modelling/components/distance_map_component.ts b/app/javascript/projects/modelling/components/distance_map_component.ts index 662963f4..68274ea2 100644 --- a/app/javascript/projects/modelling/components/distance_map_component.ts +++ b/app/javascript/projects/modelling/components/distance_map_component.ts @@ -5,15 +5,26 @@ import { PreviewControl } from '../controls/preview' import { BooleanTileGrid, NumericTileGrid } from '../tile_grid' import { booleanDataSocket, numericDataSocket } from '../socket_types' import { workerPool } from '../../../modelling/workerPool' -import { currentExtent } from '../bounding_box' +import { currentExtent, maskFromExtentAndShape } from '../bounding_box' +import { Extent } from 'ol/extent' export class DistanceMapComponent extends BaseComponent { cache: Map + maskMode: boolean + maskLayer: string + maskCQL: string + projectExtent: Extent + projectZoom: number - constructor() { + constructor(projectExtent: Extent , projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { super('Distance map') this.category = "Calculations" this.cache = new Map() + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL + this.projectExtent = projectExtent + this.projectZoom = projectZoom } async builder(node: Node) { @@ -28,6 +39,9 @@ export class DistanceMapComponent extends BaseComponent { let editorNode = this.editor?.nodes.find(n => n.id === node.id) if (editorNode === undefined) { return } + + const mask = await maskFromExtentAndShape(this.projectExtent, this.projectZoom, this.maskLayer, this.maskCQL, this.maskMode) + if (inputs['in'].length === 0) { editorNode.meta.errorMessage = 'No input' } else { @@ -45,7 +59,7 @@ export class DistanceMapComponent extends BaseComponent { } else { editorNode.meta.previousInput = input editorNode.meta.output = outputs['out'] = await workerPool.queue(async worker => - worker.generateDistanceMap(input) + worker.generateDistanceMap(input, mask) ) } this.cache.set(inputs['in'][0] as BooleanTileGrid, editorNode.meta.output as NumericTileGrid) diff --git a/app/javascript/projects/modelling/components/index.ts b/app/javascript/projects/modelling/components/index.ts index 5c0b5915..3760bce6 100644 --- a/app/javascript/projects/modelling/components/index.ts +++ b/app/javascript/projects/modelling/components/index.ts @@ -34,24 +34,24 @@ import { ATIComponent } from "./ati_component" import { DesignationsComponent } from "./designations_component" import { ORValComponent } from "./orval_component" -export function createDefaultComponents(saveMapLayer: SaveMapLayer, saveModel: SaveModel, getDatasets: getDatasets, extent: Extent, zoom: number): BaseComponent[] { +export function createDefaultComponents(saveMapLayer: SaveMapLayer, saveModel: SaveModel, getDatasets: getDatasets, extent: Extent, zoom: number, mask: boolean, maskLayer: string, maskCQL: string): BaseComponent[] { return [ // Inputs - new UkcehLandCoverComponent(extent, zoom), - new LehLandCoverComponent(extent, zoom), - new MlTreeHedgeComponent(extent, zoom), - new BiodiversityComponent(extent, zoom), - new NevoLayerComponent(extent, zoom), - new ORValComponent(extent, zoom), - new OSMLandUseComponent(extent, zoom), + new UkcehLandCoverComponent(extent, zoom, mask, maskLayer, maskCQL), + new LehLandCoverComponent(extent, zoom, mask, maskLayer, maskCQL), + new MlTreeHedgeComponent(extent, zoom, mask, maskLayer, maskCQL), + new BiodiversityComponent(extent, zoom, mask, maskLayer, maskCQL), + new NevoLayerComponent(extent, zoom, mask, maskLayer, maskCQL), + new ORValComponent(extent, zoom, mask, maskLayer, maskCQL), + new OSMLandUseComponent(extent, zoom, mask, maskLayer, maskCQL), new NumericConstantComponent(), - new DigitalModelComponent(extent, zoom), - new PrecompiledModelComponent(getDatasets, extent, zoom), //TODO: Work out how this should use project extent and zoom - new CensusComponent(extent, zoom), - new OSGreenSpacesComponent(extent, zoom), - new CROMEComponent(extent, zoom), - new ATIComponent(extent, zoom), - new DesignationsComponent(extent, zoom), + new DigitalModelComponent(extent, zoom, mask, maskLayer, maskCQL), + new PrecompiledModelComponent(getDatasets, extent, zoom, mask, maskLayer, maskCQL), + new CensusComponent(extent, zoom, mask, maskLayer, maskCQL), + new OSGreenSpacesComponent(extent, zoom, mask, maskLayer, maskCQL), + new CROMEComponent(extent, zoom, mask, maskLayer, maskCQL), + new ATIComponent(extent, zoom, mask, maskLayer, maskCQL), + new DesignationsComponent(extent, zoom, mask, maskLayer, maskCQL), // Outputs new MapLayerComponent(saveMapLayer), @@ -64,7 +64,7 @@ export function createDefaultComponents(saveMapLayer: SaveMapLayer, saveModel: S // Calculations new AreaComponent(), - new DistanceMapComponent(), + new DistanceMapComponent(extent, zoom, mask, maskLayer, maskCQL), new ScaleFactorComponent(), // Charts diff --git a/app/javascript/projects/modelling/components/leh_land_cover_component.ts b/app/javascript/projects/modelling/components/leh_land_cover_component.ts index f7334033..be3a2100 100644 --- a/app/javascript/projects/modelling/components/leh_land_cover_component.ts +++ b/app/javascript/projects/modelling/components/leh_land_cover_component.ts @@ -4,7 +4,7 @@ import { NodeData, WorkerInputs, WorkerOutputs } from "rete/types/core/data"; import { Node, Output, Socket } from "rete"; import { booleanDataSocket, categoricalDataSocket } from "../socket_types"; import { createXYZ } from "ol/tilegrid"; -import { bboxFromExtent } from "../bounding_box"; +import { bboxFromExtent, maskFromExtentAndShape } from "../bounding_box"; import { GeoJSON } from "ol/format"; import { BooleanTileGrid, CategoricalTileGrid } from "../tile_grid"; import { find } from "lodash"; @@ -109,7 +109,7 @@ const habitats: Habitat[] = [ ] -async function renderCategoricalData(extent: Extent, zoom: number) : Promise { +async function renderCategoricalData(extent: Extent, zoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) : Promise { const tileGrid = createXYZ() const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom) @@ -127,6 +127,9 @@ async function renderCategoricalData(extent: Extent, zoom: number) : Promise - - constructor(projectExtent: Extent, projectZoom: number) { - super("Living England Land Cover") - this.category = "Inputs" - this.projectExtent = projectExtent - this.projectZoom = projectZoom - this.categoricalData = null - this.outputCache = new Map() - } + maskMode: boolean + maskLayer: string + maskCQL: string + + constructor(projectExtent: Extent, projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { + super("Living England Land Cover") + this.category = "Inputs" + this.projectExtent = projectExtent + this.projectZoom = projectZoom + this.categoricalData = null + this.outputCache = new Map() + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL + } async builder(node: Node) { node.meta.toolTip = "Living England is a multi-year project which delivers a habitat probability map for the whole of England, created using satellite imagery, field data records and other geospatial data in a machine learning framework. The Living England habitat probability map shows the extent and distribution of broad habitats across England." @@ -208,7 +217,7 @@ export class LehLandCoverComponent extends BaseComponent { async worker(node: NodeData, inputs: WorkerInputs, outputs: WorkerOutputs, ...args: unknown[]) { if (this.categoricalData === null) { - this.categoricalData = await renderCategoricalData(this.projectExtent, this.projectZoom) + this.categoricalData = await renderCategoricalData(this.projectExtent, this.projectZoom, this.maskMode, this.maskLayer, this.maskCQL) } const categoricalData = this.categoricalData! diff --git a/app/javascript/projects/modelling/components/ml_tree_hedge_component.ts b/app/javascript/projects/modelling/components/ml_tree_hedge_component.ts index 6d10fbe6..8181d3a4 100644 --- a/app/javascript/projects/modelling/components/ml_tree_hedge_component.ts +++ b/app/javascript/projects/modelling/components/ml_tree_hedge_component.ts @@ -7,6 +7,7 @@ import { BaseComponent } from "./base_component" import { retrieveModelDataWCS } from "../model_retrieval" import { TypedArray } from "d3" import { Extent } from "ol/extent" +import { maskFromExtentAndShape } from "../bounding_box" interface Habitat { agg: number @@ -22,9 +23,11 @@ const habitats: Habitat[] = [ { agg: 2, AC: "Tree", mode: 2, LC: "Tree" } ] -async function renderCategoricalData(extent: Extent, zoom: number) { +async function renderCategoricalData(extent: Extent, zoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { // When testing locally, disable CORS in browser settings + const mask = await maskFromExtentAndShape(extent, zoom, maskLayer, maskCQL, maskMode) + const tileGrid = createXYZ() const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom) @@ -53,7 +56,7 @@ async function renderCategoricalData(extent: Extent, zoom: number) { let x = (outputTileRange.minX + i % image.getWidth()) let y = (outputTileRange.minY + Math.floor(i / image.getWidth())) - result.set(x, y, rasters[0][i]) + result.set(x, y, mask.get(x, y) === true ? rasters[0][i] : 255) } @@ -67,14 +70,20 @@ export class MlTreeHedgeComponent extends BaseComponent { outputCache: Map projectExtent: Extent zoom: number + maskMode: boolean + maskLayer: string + maskCQL: string - constructor(projectExtent: Extent, projectZoom: number) { + constructor(projectExtent: Extent, projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { super("ML Model Output") this.category = "Inputs" this.categoricalData = null this.outputCache = new Map() this.projectExtent = projectExtent this.zoom = projectZoom + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL } async builder(node: Node) { @@ -90,7 +99,7 @@ export class MlTreeHedgeComponent extends BaseComponent { async worker(node: NodeData, inputs: WorkerInputs, outputs: WorkerOutputs, ...args: unknown[]) { if (this.categoricalData === null) { - this.categoricalData = await renderCategoricalData(this.projectExtent, this.zoom) + this.categoricalData = await renderCategoricalData(this.projectExtent, this.zoom, this.maskMode, this.maskLayer, this.maskCQL) } const categoricalData = this.categoricalData! diff --git a/app/javascript/projects/modelling/components/nevo_layer_component.ts b/app/javascript/projects/modelling/components/nevo_layer_component.ts index f5921dd7..ef706f2c 100644 --- a/app/javascript/projects/modelling/components/nevo_layer_component.ts +++ b/app/javascript/projects/modelling/components/nevo_layer_component.ts @@ -10,7 +10,7 @@ import { Extent, getArea } from "ol/extent" import { SelectControl } from "../controls/select" import { Geometry } from "ol/geom" import { Feature } from "ol" -import { bboxFromExtent } from "../bounding_box" +import { bboxFromExtent, maskFromExtentAndShape } from "../bounding_box" interface LayerProperty { @@ -2040,13 +2040,19 @@ export class NevoLayerComponent extends BaseComponent { outputCache: Map projectExtent: Extent projectZoom: number + maskMode: boolean + maskLayer: string + maskCQL: string - constructor(projectExtent: Extent, projectZoom: number) { + constructor(projectExtent: Extent, projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { super("NEVO layer") this.category = "Inputs" this.projectExtent = projectExtent this.projectZoom = projectZoom + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL } async builder(node: Node) { @@ -2087,6 +2093,8 @@ export class NevoLayerComponent extends BaseComponent { this.nevoOutput = new GeoJSON().readFeatures(json) } + const mask = await maskFromExtentAndShape(this.projectExtent, this.projectZoom, this.maskLayer, this.maskCQL, this.maskMode) + const features = this.nevoOutput const tileGrid = createXYZ() @@ -2144,7 +2152,7 @@ export class NevoLayerComponent extends BaseComponent { const factor = tileArea / featureArea - result.set(x, y, val * factor) + result.set(x, y, mask.get(x, y) ? val * factor : NaN) } } } diff --git a/app/javascript/projects/modelling/components/orval_component.ts b/app/javascript/projects/modelling/components/orval_component.ts index db7dadf1..1abba4a9 100644 --- a/app/javascript/projects/modelling/components/orval_component.ts +++ b/app/javascript/projects/modelling/components/orval_component.ts @@ -7,7 +7,7 @@ import { createXYZ } from "ol/tilegrid"; import { retrieveModelData } from "../model_retrieval" import { BooleanTileGrid } from "../tile_grid" import { TypedArray } from "d3" -import { bboxFromExtent } from "../bounding_box" +import { bboxFromExtent, maskFromExtentAndShape } from "../bounding_box" import { GeoJSON } from "ol/format" import { Feature } from "ol" import { Geometry } from "ol/geom" @@ -20,7 +20,7 @@ interface OutputData { prettyName: string socket: Socket layer: string - fn: (extent: Extent, zoom: number, type: string, layer: string) => Promise + fn: (extent: Extent, zoom: number, type: string, layer: string, maskMode: boolean, maskCQL: string, maskLayer: string) => Promise } const OutputDatas : OutputData[] = [ @@ -187,7 +187,10 @@ const OutputDatas : OutputData[] = [ } ] -async function retrieveCatData(extent: Extent, zoom: number, type: string, layer: string) { +async function retrieveCatData(extent: Extent, zoom: number, type: string, layer: string, maskMode: boolean, maskCQL: string, maskLayer: string) { + + + const mask = await maskFromExtentAndShape(extent, zoom, maskLayer, maskCQL, maskMode) const tileGrid = createXYZ() const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom) @@ -244,7 +247,7 @@ async function retrieveCatData(extent: Extent, zoom: number, type: string, layer ) { const center = tileGrid.getTileCoordCenter([zoom, x, y]) if (geom.intersectsCoordinate(center) && feature.get("TYPE") === type) { - result.set(x, y, true) + result.set(x, y, mask.get(x, y)) } } } @@ -254,7 +257,8 @@ async function retrieveCatData(extent: Extent, zoom: number, type: string, layer return result } -async function retrievePathData(extent: Extent, zoom: number, type: string, layer: string) { +async function retrievePathData(extent: Extent, zoom: number, type: string, layer: string, maskMode: boolean, maskCQL: string, maskLayer: string) { + const mask = await maskFromExtentAndShape(extent, zoom, maskLayer, maskCQL, maskMode) const tileGrid = createXYZ() const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom) @@ -276,7 +280,7 @@ async function retrievePathData(extent: Extent, zoom: number, type: string, laye let x = (outputTileRange.minX + i % image.getWidth()) let y = (outputTileRange.minY + Math.floor(i / image.getWidth())) - result.set(x, y, rasters[3][i]) + result.set(x, y, mask.get(x,y) === true ? rasters[3][i] : false) } @@ -287,13 +291,19 @@ export class ORValComponent extends BaseComponent { projectExtent: Extent projectZoom: number outputCache: Map + maskMode: boolean + maskLayer: string + maskCQL: string - constructor(projectExtent: Extent, projectZoom: number) { + constructor(projectExtent: Extent, projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { super("ORVal") this.category = "Inputs" this.projectExtent = projectExtent this.projectZoom = projectZoom this.outputCache = new Map() + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL } async builder(node: Node) { @@ -309,7 +319,7 @@ export class ORValComponent extends BaseComponent { const promises = OutputDatas.filter(d => node.outputs[d.name].connections.length > 0) .map(d => { - return this.outputCache.has(d.name) ? this.outputCache.get(d.name) : d.fn(this.projectExtent, this.projectZoom, d.name, d.layer) + return this.outputCache.has(d.name) ? this.outputCache.get(d.name) : d.fn(this.projectExtent, this.projectZoom, d.name, d.layer, this.maskMode, this.maskCQL, this.maskLayer) .then(data => { data.name = d.name this.outputCache.set(d.name, data) diff --git a/app/javascript/projects/modelling/components/os_greenspaces_component.ts b/app/javascript/projects/modelling/components/os_greenspaces_component.ts index 0cc4c9fe..6d948f04 100644 --- a/app/javascript/projects/modelling/components/os_greenspaces_component.ts +++ b/app/javascript/projects/modelling/components/os_greenspaces_component.ts @@ -6,7 +6,7 @@ import GeoJSON from "ol/format/GeoJSON" import { createXYZ } from "ol/tilegrid" import { BooleanTileGrid } from "../tile_grid" import { Extent } from "ol/extent" -import { bboxFromExtent } from "../bounding_box" +import { bboxFromExtent, maskFromExtentAndShape } from "../bounding_box" interface GS_Source { source: string @@ -52,13 +52,19 @@ export class OSGreenSpacesComponent extends BaseComponent { cachedData: Map projectExtent: Extent projectZoom: number + maskMode: boolean + maskLayer: string + maskCQL: string - constructor(currentExtent: Extent, projectZoom: number) { + constructor(currentExtent: Extent, projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { super('OS Green Spaces') this.category = 'Inputs' this.cachedData = new Map() this.projectExtent = currentExtent this.projectZoom = projectZoom + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL } async builder(node: Node) { @@ -71,6 +77,8 @@ export class OSGreenSpacesComponent extends BaseComponent { const editorNode = this.editor?.nodes.find(n => n.id === node.id) if (editorNode === undefined) { return } + + const mask = await maskFromExtentAndShape(this.projectExtent, this.projectZoom, this.maskLayer, this.maskCQL, this.maskMode) for (let i = 0; i < GS_Sources.length; i++) { @@ -116,7 +124,7 @@ export class OSGreenSpacesComponent extends BaseComponent { ++y ) { const center = tileGrid.getTileCoordCenter([this.projectZoom, x, y]) - if (geom.intersectsCoordinate(center)) { + if (geom.intersectsCoordinate(center) && mask.get(x, y)) { result.set(x, y, true) } } diff --git a/app/javascript/projects/modelling/components/osm_land_use_component.ts b/app/javascript/projects/modelling/components/osm_land_use_component.ts index 9d417ab7..8688543d 100644 --- a/app/javascript/projects/modelling/components/osm_land_use_component.ts +++ b/app/javascript/projects/modelling/components/osm_land_use_component.ts @@ -9,6 +9,7 @@ import { Extent } from "ol/extent" import { createXYZ } from "ol/tilegrid" import * as proj4 from "proj4" import { Point, Polygon } from "ol/geom" +import { maskFromExtentAndShape } from "../bounding_box" @@ -299,12 +300,18 @@ export class OSMLandUseComponent extends BaseComponent { outputCache: Map projectZoom: number projectExtent: Extent + maskMode: boolean + maskLayer: string + maskCQL: string - constructor(projectExtent: Extent, projectZoom: number) { + constructor(projectExtent: Extent, projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { super("OSM land use layer") this.projectExtent = projectExtent this.projectZoom = projectZoom this.category = "Inputs" + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL } @@ -352,6 +359,8 @@ export class OSMLandUseComponent extends BaseComponent { const result = editorNode.meta.output = outputs['out'] = this.outputCache.get(code) } else { + const mask = await maskFromExtentAndShape(this.projectExtent, this.projectZoom, this.maskLayer, this.maskCQL, this.maskMode) + const json = await retrieveLandUseData(this.projectExtent, code, type) const features = json.elements as Array @@ -411,7 +420,7 @@ export class OSMLandUseComponent extends BaseComponent { ) { const tileExtent = tileGrid.getTileCoordExtent([this.projectZoom, x, y]) if (polygon.intersectsExtent(tileExtent)) { - result.set(x, y, true) + result.set(x, y, mask.get(x, y) === true) } } } diff --git a/app/javascript/projects/modelling/components/ukceh_land_cover_component.ts b/app/javascript/projects/modelling/components/ukceh_land_cover_component.ts index 3ee85e14..657bbe3b 100644 --- a/app/javascript/projects/modelling/components/ukceh_land_cover_component.ts +++ b/app/javascript/projects/modelling/components/ukceh_land_cover_component.ts @@ -7,6 +7,7 @@ import { BaseComponent } from "./base_component" import { retrieveModelDataWCS } from "../model_retrieval" import { TypedArray } from "d3" import { Extent } from "ol/extent" +import { maskFromExtentAndShape } from "../bounding_box" interface Habitat { agg: number @@ -41,9 +42,11 @@ const habitats: Habitat[] = [ { agg: 0, AC: "All", mode: 0, LC: "All" } ] -async function renderCategoricalData(extent: Extent, zoom: number) { +async function renderCategoricalData(extent: Extent, zoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { // When testing locally, disable CORS in browser settings + const mask = await maskFromExtentAndShape(extent, zoom, maskLayer, maskCQL, maskMode) + const tileGrid = createXYZ() const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom) @@ -72,7 +75,7 @@ async function renderCategoricalData(extent: Extent, zoom: number) { let x = (outputTileRange.minX + i % image.getWidth()) let y = (outputTileRange.minY + Math.floor(i / image.getWidth())) - result.set(x, y, rasters[0][i]) + result.set(x, y, mask.get(x,y) ? rasters[0][i] : 255) } @@ -86,14 +89,20 @@ export class UkcehLandCoverComponent extends BaseComponent { outputCache: Map projectExtent: Extent zoom: number + maskMode: boolean + maskLayer: string + maskCQL: string - constructor(projectExtent: Extent, projectZoom: number) { + constructor(projectExtent: Extent, projectZoom: number, maskMode: boolean, maskLayer: string, maskCQL: string) { super("UKCEH Land Cover") this.category = "Inputs" this.categoricalData = null this.outputCache = new Map() this.projectExtent = projectExtent this.zoom = projectZoom + this.maskMode = maskMode + this.maskLayer = maskLayer + this.maskCQL = maskCQL } async builder(node: Node) { @@ -109,7 +118,7 @@ export class UkcehLandCoverComponent extends BaseComponent { async worker(node: NodeData, inputs: WorkerInputs, outputs: WorkerOutputs, ...args: unknown[]) { if (this.categoricalData === null) { - this.categoricalData = await renderCategoricalData(this.projectExtent, this.zoom) + this.categoricalData = await renderCategoricalData(this.projectExtent, this.zoom, this.maskMode, this.maskLayer, this.maskCQL) } const categoricalData = this.categoricalData! diff --git a/app/javascript/projects/project_editor.tsx b/app/javascript/projects/project_editor.tsx index b4cae7ec..24cbc98d 100644 --- a/app/javascript/projects/project_editor.tsx +++ b/app/javascript/projects/project_editor.tsx @@ -38,9 +38,16 @@ export function ProjectEditor({ projectId, projectSource, backButtonPath, dbMode // Retrieve the project extent from the project source, or use the current extent if not present. Calculate the zoom level from the extent. const maxTiles = 10000000 + + // retrieve extent and zoom from project source const projectExtent: Extent = projectSource.extent ? [Math.min(projectSource.extent[0], projectSource.extent[2]), Math.min(projectSource.extent[1], projectSource.extent[3]), Math.max(projectSource.extent[0], projectSource.extent[2]), Math.max(projectSource.extent[1], projectSource.extent[3])] : currentExtent const projectZoom: number = zoomFromExtent(projectExtent, maxTiles) + // if project uses a mask, set the mask source and CQL filter + const projectMask: boolean = projectSource.layer ? projectSource.layer !== "" : false + const projectMaskSource: string = projectSource.layer ? projectSource.layer : "" + const projectMaskCQL: string = projectSource.cql ? projectSource.cql : "" + const [sidebarVisible, setSidebarVisible] = React.useState(true) const [layerPaletteVisible, setLayerPaletteVisible] = React.useState(false) const [currentTab, setCurrentTab] = React.useState(Tab.MapView) @@ -239,6 +246,9 @@ export function ProjectEditor({ projectId, projectSource, backButtonPath, dbMode getDatasets={() => getDatasets(teamId)} extent={projectExtent} zoom={projectZoom} + mask={projectMask} + maskLayer={projectMaskSource} + maskCQL={projectMaskCQL} /> diff --git a/app/javascript/projects/state.ts b/app/javascript/projects/state.ts index 70e1b171..a09d7226 100644 --- a/app/javascript/projects/state.ts +++ b/app/javascript/projects/state.ts @@ -126,6 +126,8 @@ export type Layer = OsmLayer | MapTileLayer | OverlayLayer | NevoLayer | CehLand export interface Project { name: string extent?: Extent + layer?: string + cql?: string layers: Record allLayers: number[] model: Data | null diff --git a/app/models/mask.rb b/app/models/mask.rb new file mode 100644 index 00000000..4d95c14e --- /dev/null +++ b/app/models/mask.rb @@ -0,0 +1,3 @@ +class Mask < ApplicationRecord + has_one_attached :file +end \ No newline at end of file diff --git a/app/models/project.rb b/app/models/project.rb index f58d4f10..2007ceae 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -20,6 +20,22 @@ def extent=(extent) source["extent"] = extent end + def cql + source["cql"] + end + + def cql=(cql) + source["cql"] = cql + end + + def layer + source["layer"] + end + + def layer=(layer) + source["layer"] = layer + end + def parse_extent if extent.present? extent_array = extent.is_a?(String) ? extent.split(',').map(&:to_f) : extent.map(&:to_f) diff --git a/app/views/projects/_form.html.erb b/app/views/projects/_form.html.erb index 4cbfb316..16072173 100644 --- a/app/views/projects/_form.html.erb +++ b/app/views/projects/_form.html.erb @@ -29,7 +29,12 @@ - +
+ <%= form.hidden_field :layer, class: 'form-control', value: '' %> + <%= form.hidden_field :cql, class: 'form-control', value: '' %> +
+ +
@@ -46,13 +51,33 @@