Skip to content

Commit

Permalink
wip: recalculate ancestors' bbox/boundary on crag's lat/lng change
Browse files Browse the repository at this point in the history
  • Loading branch information
viet nguyen committed Jan 26, 2024
1 parent 9518cf1 commit 6c0a1a4
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 23 deletions.
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@
"clean": "tsc -b --clean && rm -rf build/*",
"serve": "yarn build && node --experimental-json-modules build/main.js",
"serve-dev": "echo \"🚨 LOCAL_DEV_BYPASS_AUTH enabled 🚨\" && LOCAL_DEV_BYPASS_AUTH=true yarn serve",
"refresh-db": "./refresh-db.sh",
"seed-usa": "yarn build && node build/db/import/usa/USADay0Seed.js",
"seed-db": "./seed-db.sh",
"add-countries": "yarn build && node build/db/utils/jobs/AddCountriesJob.js",
"update-stats": "yarn build && node build/db/utils/jobs/UpdateStatsJob.js",
Expand Down Expand Up @@ -106,4 +104,4 @@
"engines": {
"node": ">=16.14.0"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import bboxFromGeojson from '@turf/bbox'
import convexHull from '@turf/convex'
import pLimit from 'p-limit'

import { getAreaModel } from '../../AreaSchema.js'
import { AreaType, AggregateType } from '../../AreaTypes.js'
import { areaDensity } from '../../../geo-utils.js'
import { mergeAggregates } from '../Aggregate.js'
import { getAreaModel } from '../../../AreaSchema.js'
import { AreaType, AggregateType } from '../../../AreaTypes.js'
import { areaDensity } from '../../../../geo-utils.js'
import { mergeAggregates } from '../../Aggregate.js'
import { ChangeRecordMetadataType } from '../../../../db/ChangeLogType.js'

const limiter = pLimit(1000)

Expand All @@ -31,7 +32,7 @@ type AreaMongoType = mongoose.Document<unknown, any, AreaType> & AreaType
* create a new bottom-up traversal, starting from the updated node/area and bubble the
* update up to its parent.
*/
export const visitAllAreas = async (): Promise<void> => {
export const updateAllAreas = async (): Promise<void> => {
const areaModel = getAreaModel('areas')

// Step 1: Start with 2nd level of tree, eg 'state' or 'province' level and recursively update all nodes.
Expand All @@ -57,7 +58,7 @@ export const visitAllAreas = async (): Promise<void> => {
}
}

interface ResultType {
export interface StatsAccumulator {
density: number
totalClimbs: number
bbox?: BBox
Expand All @@ -66,7 +67,7 @@ interface ResultType {
polygon?: Polygon
}

async function postOrderVisit (node: AreaMongoType): Promise<ResultType> {
async function postOrderVisit (node: AreaMongoType): Promise<StatsAccumulator> {
if (node.metadata.leaf || node.children.length === 0) {
return leafReducer((node.toObject() as AreaType))
}
Expand All @@ -91,7 +92,7 @@ async function postOrderVisit (node: AreaMongoType): Promise<ResultType> {
* @param node leaf area/crag
* @returns aggregate type
*/
const leafReducer = (node: AreaType): ResultType => {
export const leafReducer = (node: AreaType): StatsAccumulator => {
return {
totalClimbs: node.totalClimbs,
bbox: node.metadata.bbox,
Expand All @@ -115,7 +116,7 @@ const leafReducer = (node: AreaType): ResultType => {
/**
* Calculate convex hull polyon contain all child areas
*/
const calculatePolygonFromChildren = (nodes: ResultType[]): Feature<Polygon> | null => {
const calculatePolygonFromChildren = (nodes: StatsAccumulator[]): Feature<Polygon> | null => {
const childAsPolygons = nodes.reduce<Array<Feature<Polygon>>>((acc, curr) => {
if (curr.bbox != null) {
acc.push(bbox2Polygon(curr.bbox))
Expand All @@ -127,14 +128,19 @@ const calculatePolygonFromChildren = (nodes: ResultType[]): Feature<Polygon> | n
return polygonFeature
}

interface OPTIONS {
session: mongoose.ClientSession
changeRecord: ChangeRecordMetadataType
}

/**
* Calculate stats from a list of nodes
* @param result nodes
* @param parent parent node to save stats to
* @returns Calculated stats
*/
const nodesReducer = async (result: ResultType[], parent: AreaMongoType): Promise<ResultType> => {
const initial: ResultType = {
export const nodesReducer = async (result: StatsAccumulator[], parent: AreaMongoType, options?: OPTIONS): Promise<StatsAccumulator> => {
const initial: StatsAccumulator = {
totalClimbs: 0,
bbox: undefined,
lnglat: undefined,
Expand All @@ -152,14 +158,12 @@ const nodesReducer = async (result: ResultType[], parent: AreaMongoType): Promis
}
}
}
let nodeSummary: ResultType = initial
let nodeSummary: StatsAccumulator = initial
if (result.length === 0) {
const { totalClimbs, aggregate, density } = initial
parent.totalClimbs = totalClimbs
parent.density = density
parent.aggregate = aggregate
await parent.save()
return initial
} else {
nodeSummary = result.reduce((acc, curr) => {
const { totalClimbs, aggregate, lnglat, bbox } = curr
Expand All @@ -185,7 +189,15 @@ const nodesReducer = async (result: ResultType[], parent: AreaMongoType): Promis
parent.density = density
parent.aggregate = aggregate
parent.metadata.polygon = nodeSummary.polygon
}

if (options != null) {
const { session, changeRecord } = options
parent._change = changeRecord
parent.updatedBy = changeRecord.user
await parent.save({ session })
} else {
await parent.save()
return nodeSummary
}
return nodeSummary
}
4 changes: 2 additions & 2 deletions src/db/utils/jobs/UpdateStatsJob.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { connectDB, gracefulExit } from '../../index.js'
import { visitAllAreas } from './TreeUpdater.js'
import { updateAllAreas } from './TreeUpdaters/updateAllAreas.js'
import { visitAllCrags } from './CragUpdater.js'
import { logger } from '../../../logger.js'

const onConnected = async (): Promise<void> => {
logger.info('Initializing database')
console.time('Calculating global stats')
await visitAllCrags()
await visitAllAreas()
await updateAllAreas()
console.timeEnd('Calculating global stats')
await gracefulExit()
return await Promise.resolve()
Expand Down
63 changes: 60 additions & 3 deletions src/model/MutableAreaDataSource.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { geometry } from '@turf/helpers'
import { Point, geometry } from '@turf/helpers'
import muuid, { MUUID } from 'uuid-mongodb'
import { v5 as uuidv5, NIL } from 'uuid'
import mongoose, { ClientSession } from 'mongoose'
import { produce } from 'immer'
import { UserInputError } from 'apollo-server'
import isoCountries from 'i18n-iso-countries'
import enJson from 'i18n-iso-countries/langs/en.json' assert { type: 'json' }
import bbox2Polygon from '@turf/bbox-polygon'

import { AreaType, AreaEditableFieldsType, OperationType, UpdateSortingOrderType } from '../db/AreaTypes.js'
import AreaDataSource from './AreaDataSource.js'
Expand All @@ -19,6 +20,8 @@ import { GradeContexts } from '../GradeUtils.js'
import { sanitizeStrict } from '../utils/sanitize.js'
import { ExperimentalAuthorType } from '../db/UserTypes.js'
import { createInstance as createExperimentalUserDataSource } from '../model/ExperimentalUserDataSource.js'
import { StatsAccumulator, leafReducer, nodesReducer } from '../db/utils/jobs/TreeUpdaters/updateAllAreas.js'
import { bboxFrom } from '../geo-utils.js'

isoCountries.registerLocale(enJson)

Expand Down Expand Up @@ -363,10 +366,20 @@ export default class MutableAreaDataSource extends AreaDataSource {
area.set({ 'content.description': sanitized })
}

if (lat != null && lng != null) { // we should already validate lat,lng before in GQL layer
const latLngHasChanged = lat != null && lng != null
if (latLngHasChanged) { // we should already validate lat,lng before in GQL layer
const point = geometry('Point', [lng, lat]) as Point
area.set({
'metadata.lnglat': geometry('Point', [lng, lat])
'metadata.lnglat': point
})
if (area.metadata.leaf || (area.metadata?.isBoulder ?? false)) {
const bbox = bboxFrom(point)
area.set({
'metadata.bbox': bbox,
'metadata.polygon': bbox == null ? undefined : bbox2Polygon(bbox).geometry
})
await this.updateStatsAndGeoDataForSinglePath(session, _change, area)
}
}

const cursor = await area.save()
Expand Down Expand Up @@ -471,6 +484,50 @@ export default class MutableAreaDataSource extends AreaDataSource {
return ret
}

/**
* Update area stats and geo data for a given leaf node and its ancestors
* @param session
* @param changeRecord
* @param area
*/
async updateStatsAndGeoDataForSinglePath (session: ClientSession, changeRecord: ChangeRecordMetadataType, area: AreaDocumnent): Promise<void> {
const visitorFn = async (session: ClientSession, changeRecord: ChangeRecordMetadataType, area: AreaDocumnent, accumulator: StatsAccumulator): Promise<void> => {
if (area.pathTokens.length <= 1) {
return
}

const ancestors = area.ancestors.split(',')
const parentUuid = muuid.from(ancestors[ancestors.length - 2])
const parentArea =
await this.areaModel.findOne({ 'metadata.area_id': parentUuid })
.batchSize(10)
.populate<{ children: AreaDocumnent[] }>({ path: 'children', model: this.areaModel })
.allowDiskUse(true)
.session(session)
.orFail()

logger.info(`###Updating stats for ${parentArea.area_name}`)
logger.info(` ##prev Area ${area._id} ${area.area_name}`)

const acc: StatsAccumulator[] = []
for (const childArea of parentArea.children) {
logger.info(` - ${childArea._id} ${childArea.area_name}`)

if (childArea._id.equals(area._id)) {
acc.push(accumulator)
} else {
acc.push(leafReducer(childArea.toObject()))
}
}

const current = await nodesReducer(acc, parentArea as any as AreaDocumnent, { session, changeRecord })

await visitorFn(session, changeRecord, parentArea as any as AreaDocumnent, current)
}
const accumulator = leafReducer(area.toObject())
await visitorFn(session, changeRecord, area, accumulator)
}

static instance: MutableAreaDataSource

static getInstance (): MutableAreaDataSource {
Expand Down

0 comments on commit 6c0a1a4

Please sign in to comment.