Skip to content

Commit

Permalink
Merge pull request #372 from wearepal/shapes-as-extent
Browse files Browse the repository at this point in the history
Users can choose from predefined extents with a shape mask
  • Loading branch information
paulthatjazz authored May 28, 2024
2 parents 528066f + 611a881 commit 695d0ae
Show file tree
Hide file tree
Showing 35 changed files with 550 additions and 98 deletions.
3 changes: 3 additions & 0 deletions app/assets/stylesheets/masks.scss
Original file line number Diff line number Diff line change
@@ -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/
24 changes: 24 additions & 0 deletions app/controllers/masks_controller.rb
Original file line number Diff line number Diff line change
@@ -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
7 changes: 5 additions & 2 deletions app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions app/helpers/masks_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module MasksHelper
end
4 changes: 2 additions & 2 deletions app/javascript/modelling/worker/generateDistanceMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
}

Expand Down
7 changes: 5 additions & 2 deletions app/javascript/projects/model_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement>(null)
const [editor, setEditor] = React.useState<NodeEditor>()
const [engine, setEngine] = React.useState<Engine>()
Expand All @@ -65,7 +68,7 @@ export function ModelView({ visible, initialTransform, setTransform, initialMode
})

const engine = new Engine("[email protected]")
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)
})
Expand Down
144 changes: 143 additions & 1 deletion app/javascript/projects/modelling/bounding_box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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`
}
}

const maskMap = new Map<string, BooleanTileGrid>()

export async function maskFromExtentAndShape(extent: Extent, zoom: number, shapeLayer: string, shapeId: string, maskMode: boolean = false): Promise<BooleanTileGrid> {
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<BooleanTileGrid | null> {
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()
});
}
18 changes: 13 additions & 5 deletions app/javascript/projects/modelling/components/ati_component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -54,7 +54,7 @@ const trees: TreeType[] = [
]


async function renderCategoricalData(extent: Extent, zoom: number) : Promise<CategoricalTileGrid>{
async function renderCategoricalData(extent: Extent, zoom: number, maskLayer: string, maskCQL: string, maskMode: boolean) : Promise<CategoricalTileGrid>{

const tileGrid = createXYZ()
const outputTileRange = tileGrid.getTileRangeForExtentAndZ(extent, zoom)
Expand All @@ -71,6 +71,8 @@ async function renderCategoricalData(extent: Extent, zoom: number) : Promise<Cat
}
)
)

const mask = await maskFromExtentAndShape(extent, zoom, maskLayer, maskCQL, maskMode)

if (!response.ok) throw new Error()

Expand Down Expand Up @@ -114,7 +116,7 @@ async function renderCategoricalData(extent: Extent, zoom: number) : Promise<Cat
y <= Math.min(featureTileRange.maxY, outputTileRange.maxY);
++y
) {
result.set(x, y, key?.id ?? 255)
result.set(x, y, mask.get(x, y) ? key?.id ?? 255 : 255)
}
}

Expand All @@ -137,14 +139,20 @@ export class ATIComponent extends BaseComponent {
projectExtent: Extent
projectZoom: number
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("Ancient Tree Inventory")
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
}


Expand All @@ -166,7 +174,7 @@ export class ATIComponent 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.maskLayer, this.maskCQL, this.maskMode)
}
const categoricalData = this.categoricalData!

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { NumericTileGrid, toIndex } from '../tile_grid'
import { createXYZ } from 'ol/tilegrid'
import { Coordinate } from 'ol/coordinate'
import { Point } from 'ol/geom'
import { EPSG3857, EPSG4326, WKTfromExtent } from '../bounding_box'
import { EPSG3857, EPSG4326, WKTfromExtent, maskFromExtentAndShape } from '../bounding_box'
import * as proj4 from 'proj4'
import { speciesFamilyList, speciesList } from '../nbnatlas_species'

Expand Down Expand Up @@ -72,12 +72,18 @@ async function fetchSpeciesFromExtent(extent: Extent, familyId: number, selected
export class BiodiversityComponent extends BaseComponent {
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('Recorded species')
this.category = 'Inputs'
this.projectExtent = projectExtent
this.projectZoom = projectZoom
this.maskMode = maskMode
this.maskLayer = maskLayer
this.maskCQL = maskCQL
}

async loadSpeciesFamilyList(node: Node) {
Expand Down Expand Up @@ -138,6 +144,9 @@ export class BiodiversityComponent 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)

const speciesFamilyId = node.data.speciesFamilyId || 1

const tileGrid = createXYZ()
Expand All @@ -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)
}

Expand Down
Loading

0 comments on commit 695d0ae

Please sign in to comment.