Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: make lat/lng, bbox, polygon optional #385

Merged
merged 1 commit into from
Jan 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading