Skip to content

Commit

Permalink
fix: make lat/lng, bbox, polygon optional (#385)
Browse files Browse the repository at this point in the history
  • Loading branch information
vnugent committed Jan 20, 2024
1 parent b8a709a commit 494c830
Show file tree
Hide file tree
Showing 15 changed files with 114 additions and 76 deletions.
5 changes: 3 additions & 2 deletions src/db/AreaSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ const MetadataSchema = new Schema<IAreaMetadata>({
isBoulder: { type: Boolean, default: false },
lnglat: {
type: PointSchema,
index: '2dsphere'
index: '2dsphere',
required: false
},
polygon: polygonSchema,
bbox: [{ type: Number, required: true }],
bbox: [{ type: Number, required: false }],
leftRightIndex: { type: Number, required: false },
ext_id: { type: String, required: false, index: true },
area_id: {
Expand Down
6 changes: 3 additions & 3 deletions src/db/AreaTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export interface IAreaProps extends AuthorMetadata {
* computed aggregations on this document. See the AggregateType documentation for
* more information.
*/
aggregate?: AggregateType
aggregate: AggregateType
/**
* User-composed content that makes up most of the user-readable data in the system.
* See the IAreaContent documentation for more information.
Expand Down Expand Up @@ -120,12 +120,12 @@ export interface IAreaMetadata {
/**
* Location of a wall or a boulder aka leaf node. Use `bbox` or `polygon` non-leaf areas.
* */
lnglat: Point
lnglat?: Point
/**
* The smallest possible bounding box (northwest and southeast coordinates) that contains
* all of this areas children (Both sub-areas and climbs).
*/
bbox: BBox
bbox?: BBox

/**
* Left-to-right sorting index. Undefined or -1 for unsorted area,
Expand Down
2 changes: 1 addition & 1 deletion src/db/ClimbTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export interface DisciplineType {
tr?: boolean
}
export interface IClimbMetadata {
lnglat: Point
lnglat?: Point
left_right_index?: number
/** mountainProject ID (if this climb was sourced from mountainproject) */
mp_id?: string
Expand Down
3 changes: 2 additions & 1 deletion src/db/MediaObjectSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const EntitySchema = new Schema<EntityTag>({
ancestors: { type: Schema.Types.String, required: true, index: true },
lnglat: {
type: PointSchema,
index: '2dsphere'
index: '2dsphere',
required: false
}
}, { _id: true })

Expand Down
2 changes: 1 addition & 1 deletion src/db/MediaObjectTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface EntityTag {
ancestors: string
climbName?: string
areaName: string
lnglat: Point
lnglat?: Point
}

export interface MediaByUsers {
Expand Down
4 changes: 2 additions & 2 deletions src/db/export/Typesense/TypesenseSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface ClimbTypeSenseItem {
disciplines: string[]
grade?: string // Todo: switch to grade context
safety: string
cragLatLng: [number, number]
cragLatLng?: [number, number]
}

/**
Expand Down Expand Up @@ -78,7 +78,7 @@ export interface AreaTypeSenseItem {
name: string
pathTokens: string[]
areaUUID: string
areaLatLng: [number, number]
areaLatLng?: [number, number]
leaf: boolean
isDestination: boolean
totalClimbs: number
Expand Down
5 changes: 4 additions & 1 deletion src/db/export/Typesense/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@ export const disciplinesToArray = (type: DisciplineType): any => {
* @param geoPoint
* @returns
*/
export const geoToLatLng = (geoPoint: Point): [number, number] => {
export const geoToLatLng = (geoPoint?: Point): [number, number] | undefined => {
if (geoPoint == null) {
return undefined
}
const { coordinates } = geoPoint
return [coordinates[1], coordinates[0]]
}
20 changes: 13 additions & 7 deletions src/db/utils/Aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,17 +80,23 @@ export const aggregateCragStats = (crag: AreaType): AggregateType => {
const byGrade: Record<string, number> | {} = {}
const disciplines: CountByDisciplineType = {}

const DEFAULT = {
byGrade: [],
byDiscipline: disciplines,
byGradeBand: {
...INIT_GRADEBAND
}
}

if ((crag.climbs?.length ?? 0) === 0) {
return DEFAULT
}

// Assumption: all climbs use the crag's grade context
const cragGradeScales = gradeContextToGradeScales[crag.gradeContext]
if (cragGradeScales == null) {
logger.warn(`Area ${crag.area_name} (${crag.metadata.area_id.toUUID().toString()}) has invalid grade context: '${crag.gradeContext}'`)
return {
byGrade: [],
byDiscipline: disciplines,
byGradeBand: {
...INIT_GRADEBAND
}
}
return DEFAULT
}

const climbs = crag.climbs as ClimbType[]
Expand Down
17 changes: 11 additions & 6 deletions src/db/utils/jobs/CragGeojson/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,26 @@ async function exportLeafCrags (): Promise<void> {

const features: Array<Feature<Point, {
name: string
id: string
}>> = []

for await (const doc of model.find({ 'metadata.leaf': true }).lean()) {
const { metadata, area_name: areaName, pathTokens, ancestors } = doc
for await (const doc of model.find({ 'metadata.leaf': true, 'metadata.lnglat': { $ne: null } }).lean()) {
if (doc.metadata.lnglat == null) {
continue
}

const { metadata, area_name: areaName, pathTokens, ancestors, content } = doc

const ancestorArray = ancestors.split(',')
const pointFeature = point(doc.metadata.lnglat.coordinates, {
id: metadata.area_id.toUUID().toString(),
name: areaName,
type: 'crag',
content,
parent: {
id: ancestorArray[ancestorArray.length - 2],
name: pathTokens[doc.pathTokens.length - 2]
}
}, {
id: metadata.area_id.toUUID().toString()
})
features.push(pointFeature)
}
Expand Down Expand Up @@ -112,16 +117,16 @@ async function exportCragGroups (): Promise<void> {

const features: Array<Feature<Polygon, {
name: string
id: string
}>> = []

for await (const doc of rs) {
const polygonFeature = feature(doc.polygon, {
type: 'crag-group',
name: doc.name,
id: doc.uuid.toUUID().toString(),
children: doc.childAreaList.map(({ uuid, name, leftRightIndex }) => (
{ id: uuid.toUUID().toString(), name, lr: leftRightIndex }))
}, {
id: doc.uuid.toUUID().toString()
})
features.push(polygonFeature)
}
Expand Down
13 changes: 11 additions & 2 deletions src/db/utils/jobs/CragUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import mongoose from 'mongoose'
import bbox2Polygon from '@turf/bbox-polygon'

import { getAreaModel } from '../../AreaSchema.js'
import { getClimbModel } from '../../ClimbSchema.js'
import { AreaType } from '../../AreaTypes.js'
Expand All @@ -17,7 +19,12 @@ export const visitAllCrags = async (): Promise<void> => {

// Get all crags
const iterator = areaModel
.find({ 'metadata.leaf': true }).batchSize(10)
.find({
$or: [
{ 'metadata.leaf': true },
{ children: { $exists: true, $size: 0 } }
]
}).batchSize(10)
.populate<{ climbs: ClimbType[] }>({ path: 'climbs', model: getClimbModel() })
.allowDiskUse(true)

Expand All @@ -26,7 +33,9 @@ export const visitAllCrags = async (): Promise<void> => {
for await (const crag of iterator) {
const node: AreaMongoType = crag
node.aggregate = aggregateCragStats(crag.toObject())
node.metadata.bbox = bboxFrom(node.metadata.lnglat)
const bbox = bboxFrom(node.metadata.lnglat)
node.metadata.bbox = bbox
node.metadata.polygon = bbox == null ? undefined : bbox2Polygon(bbox).geometry
await node.save()
}
}
93 changes: 53 additions & 40 deletions src/db/utils/jobs/TreeUpdater.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import mongoose from 'mongoose'
import { featureCollection, BBox, Point, Polygon } from '@turf/helpers'
import { featureCollection, BBox, Point, Polygon, Feature } from '@turf/helpers'
import bbox2Polygon from '@turf/bbox-polygon'
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 { bboxFromList, areaDensity } from '../../../geo-utils.js'
import { areaDensity } from '../../../geo-utils.js'
import { mergeAggregates } from '../Aggregate.js'

const limiter = pLimit(1000)
Expand Down Expand Up @@ -59,14 +60,14 @@ export const visitAllAreas = async (): Promise<void> => {
interface ResultType {
density: number
totalClimbs: number
bbox: BBox
lnglat: Point
bbox?: BBox
lnglat?: Point
aggregate: AggregateType
polygon?: Polygon
}

async function postOrderVisit (node: AreaMongoType): Promise<ResultType> {
if (node.metadata.leaf) {
if (node.metadata.leaf || node.children.length === 0) {
return leafReducer((node.toObject() as AreaType))
}

Expand Down Expand Up @@ -95,6 +96,7 @@ const leafReducer = (node: AreaType): ResultType => {
totalClimbs: node.totalClimbs,
bbox: node.metadata.bbox,
lnglat: node.metadata.lnglat,
polygon: node.metadata.polygon,
density: node.density,
aggregate: node.aggregate ?? {
byGrade: [],
Expand All @@ -113,12 +115,16 @@ const leafReducer = (node: AreaType): ResultType => {
/**
* Calculate convex hull polyon contain all child areas
*/
const calculatePolygonFromChildren = (nodes: ResultType[]): Polygon | undefined => {
const childAsPolygons = nodes.map(node => bbox2Polygon(node.bbox))
const calculatePolygonFromChildren = (nodes: ResultType[]): Feature<Polygon> | null => {
const childAsPolygons = nodes.reduce<Array<Feature<Polygon>>>((acc, curr) => {
if (curr.bbox != null) {
acc.push(bbox2Polygon(curr.bbox))
}
return acc
}, [])
const fc = featureCollection(childAsPolygons)
const polygonFeature = convexHull(fc)

return polygonFeature?.geometry
return polygonFeature
}

/**
Expand All @@ -130,11 +136,8 @@ const calculatePolygonFromChildren = (nodes: ResultType[]): Polygon | undefined
const nodesReducer = async (result: ResultType[], parent: AreaMongoType): Promise<ResultType> => {
const initial: ResultType = {
totalClimbs: 0,
bbox: [-180, -90, 180, 90],
lnglat: {
type: 'Point',
coordinates: [0, 0]
},
bbox: undefined,
lnglat: undefined,
polygon: undefined,
density: 0,
aggregate: {
Expand All @@ -149,30 +152,40 @@ const nodesReducer = async (result: ResultType[], parent: AreaMongoType): Promis
}
}
}

const z = result.reduce((acc, curr, index) => {
const { totalClimbs, bbox: _bbox, aggregate, lnglat } = curr
const bbox = index === 0 ? _bbox : bboxFromList([_bbox, acc.bbox])
return {
totalClimbs: acc.totalClimbs + totalClimbs,
bbox,
lnglat, // we'll calculate a new center point later
density: -1,
polygon: undefined,
aggregate: mergeAggregates(acc.aggregate, aggregate)
}
}, initial)

z.polygon = calculatePolygonFromChildren(result)
z.density = areaDensity(z.bbox, z.totalClimbs)

const { totalClimbs, bbox, density, aggregate } = z

parent.totalClimbs = totalClimbs
parent.metadata.bbox = bbox
parent.density = density
parent.aggregate = aggregate
parent.metadata.polygon = z.polygon
await parent.save()
return z
let nodeSummary: ResultType = 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
return {
totalClimbs: acc.totalClimbs + totalClimbs,
bbox,
lnglat,
density: -1,
polygon: undefined,
aggregate: mergeAggregates(acc.aggregate, aggregate)
}
}, initial)

const polygon = calculatePolygonFromChildren(result)
nodeSummary.polygon = polygon?.geometry
nodeSummary.bbox = bboxFromGeojson(polygon)
nodeSummary.density = areaDensity(nodeSummary.bbox, nodeSummary.totalClimbs)

const { totalClimbs, bbox, density, aggregate } = nodeSummary

parent.totalClimbs = totalClimbs
parent.metadata.bbox = bbox
parent.density = density
parent.aggregate = aggregate
parent.metadata.polygon = nodeSummary.polygon
await parent.save()
return nodeSummary
}
}
6 changes: 4 additions & 2 deletions src/geo-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import { BBoxType } from './types'
* @param point
* @returns
*/
export const bboxFrom = (point: Point): BBoxType => {
export const bboxFrom = (point: Point | undefined): BBoxType | undefined => {
if (point == null) return undefined
const options = { steps: 8 }
const r = 0.05 // unit=km. Hopefully this is a large enough area (but not too large) for a crag
const cir = circle(point, r, options)
Expand All @@ -33,7 +34,8 @@ export const bboxFromList = (bboxList: BBoxType[]): any => {
* @param totalClimbs
* @returns total climbs per km sq
*/
export const areaDensity = (bbox: BBoxType, totalClimbs: number): number => {
export const areaDensity = (bbox: BBoxType | undefined, totalClimbs: number): number => {
if (bbox == null) return 0
const areaInKm = area(bboxPolygon(bbox)) / 1000000
const minArea = areaInKm < 5 ? 5 : areaInKm
return totalClimbs / minArea
Expand Down
6 changes: 3 additions & 3 deletions src/graphql/schema/Area.gql
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ type AreaMetadata {
isBoulder: Boolean

"centroid latitude of this areas bounding box"
lat: Float!
lat: Float
"centroid longitude of this areas bounding box"
lng: Float!
lng: Float
"NE and SW corners of the bounding box for this area"
bbox: [Float]!
bbox: [Float]

"Left-to-right sorting index. Undefined or -1 or unsorted area."
leftRightIndex: Int
Expand Down
Loading

0 comments on commit 494c830

Please sign in to comment.