Skip to content

Commit

Permalink
wip: convert crag data to mbtiles and upload directly to Mapbox
Browse files Browse the repository at this point in the history
  • Loading branch information
viet nguyen committed Feb 7, 2024
1 parent 9c51732 commit 1808d70
Show file tree
Hide file tree
Showing 8 changed files with 1,672 additions and 27 deletions.
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"devDependencies": {
"@types/auth0": "^3.3.2",
"@types/jest": "^29.4.0",
"@types/mapbox__mapbox-sdk": "^0.14.0",
"@types/node": "^18.13.0",
"@types/supertest": "^2.0.12",
"@types/underscore": "^1.11.4",
Expand All @@ -24,9 +25,11 @@
"typescript": "4.9.5"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.507.0",
"@babel/runtime": "^7.17.2",
"@google-cloud/storage": "^6.9.5",
"@graphql-tools/schema": "^8.3.1",
"@mapbox/mapbox-sdk": "^0.15.3",
"@openbeta/sandbag": "^0.0.51",
"@turf/area": "^6.5.0",
"@turf/bbox": "^6.5.0",
Expand Down Expand Up @@ -84,7 +87,9 @@
"export-prod": "./export.sh",
"prepare": "husky install",
"import-users": "tsc ; node build/db/utils/jobs/migration/CreateUsersCollection.js",
"export-crags": "tsc ; node build/db/utils/jobs/CragGeojson/index.js"
"maptiles:export-db": "tsc ; node build/db/utils/jobs/CragGeojson/index.js",
"maptiles:generate": "tsc ; node build/db/utils/jobs/CragGeojson/generate.js",
"maptiles:upload": "tsc ; node build/db/utils/jobs/CragGeojson/upload.js"
},
"standard": {
"plugins": [
Expand Down
6 changes: 3 additions & 3 deletions src/db/MediaObjectSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const UUID_TYPE = {
}

const EntitySchema = new Schema<EntityTag>({
targetId: { ...UUID_TYPE, index: true },
targetId: { ...UUID_TYPE, index: true, transform: (v: any) => v.toUUID().toString() },
climbName: { type: Schema.Types.String },
areaName: { type: Schema.Types.String, required: true },
type: { type: Schema.Types.Number, required: true },
Expand All @@ -20,7 +20,7 @@ const EntitySchema = new Schema<EntityTag>({
index: '2dsphere',
required: false
}
}, { _id: true })
}, { _id: true, toObject: { versionKey: false } })

const schema = new Schema<MediaObject>({
userUuid: { ...UUID_TYPE, index: true },
Expand All @@ -30,7 +30,7 @@ const schema = new Schema<MediaObject>({
size: { type: Schema.Types.Number, required: true },
format: { type: Schema.Types.String, required: true },
entityTags: [EntitySchema]
}, { _id: true, timestamps: true })
}, { _id: true, timestamps: true, toJSON: { versionKey: false }, toObject: { versionKey: false } })

/**
* Additional indices
Expand Down
2 changes: 1 addition & 1 deletion src/db/MediaObjectTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface MediaObject {

export interface EntityTag {
_id: mongoose.Types.ObjectId
targetId: MUUID
targetId: MUUID | string
type: number
ancestors: string
climbName?: string
Expand Down
63 changes: 47 additions & 16 deletions src/db/utils/jobs/CragGeojson/index.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,78 @@
import { createWriteStream } from 'node:fs'
import { WriteStream, createWriteStream } from 'node:fs'
import { point, feature, featureCollection, Feature, Point, Polygon } from '@turf/helpers'
import os from 'node:os'
import { MUUID } from 'uuid-mongodb'

import { connectDB, gracefulExit, getAreaModel } from '../../../index.js'
import { connectDB, gracefulExit, getAreaModel, getClimbModel } from '../../../index.js'
import { logger } from '../../../../logger.js'
import { ClimbType } from '../../../ClimbTypes.js'
import MutableMediaDataSource from '../../../../model/MutableMediaDataSource.js'

export const WORKING_DIR = './maptiles'

/**
* Export leaf areas as Geojson. Leaf areas are crags/boulders that have climbs.
*/
async function exportLeafCrags (): Promise<void> {
const model = getAreaModel()

const stream = createWriteStream('crags.geojson', { encoding: 'utf-8' })

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

for await (const doc of model.find({ 'metadata.leaf': true, 'metadata.lnglat': { $ne: null } }).lean()) {
let fileIndex = 0
let stream: WriteStream = createWriteStream(`crags.${fileIndex}.geojson`, { encoding: 'utf-8' })
const cursor = model.find({ 'metadata.leaf': true, 'metadata.lnglat': { $ne: null } })
.populate<{ climbs: ClimbType[] }>({ path: 'climbs', model: getClimbModel() })
.batchSize(10)
.allowDiskUse(true)
.lean()

for await (const doc of cursor) {
if (doc.metadata.lnglat == null) {
continue
}

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

const ancestorArray = ancestors.split(',')
const pointFeature = point(doc.metadata.lnglat.coordinates, {
id: metadata.area_id.toUUID().toString(),
id: metadata.area_id,
name: areaName,
type: 'crag',
content,
parent: {
id: ancestorArray[ancestorArray.length - 2],
name: pathTokens[doc.pathTokens.length - 2]
}
media: await MutableMediaDataSource.getInstance().findMediaByAreaId(metadata.area_id, ancestors),
climbs: climbs.map(({ _id, name, type, grades }: ClimbType) => ({
id: _id.toUUID().toString(),
name,
discipline: type,
grade: grades
})),
ancestors: ancestorArray,
pathTokens,
gradeContext
}, {
id: metadata.area_id.toUUID().toString()
})
features.push(pointFeature)

if (features.length === 5000) {
logger.info(`Writing file ${fileIndex}`)
stream.write(JSON.stringify(featureCollection(features)) + os.EOL)
stream.close()
features = []

fileIndex++
stream = createWriteStream(`${WORKING_DIR}/crags.${fileIndex}.geojson`, { encoding: 'utf-8' })
}
}

if (features.length > 0) {
logger.info(`Writing file ${fileIndex}`)
stream.write(JSON.stringify(featureCollection(features)) + os.EOL)
}
stream.write(JSON.stringify(featureCollection(features)) + os.EOL)
stream.close()
logger.info('Complete.')
}

/**
Expand Down Expand Up @@ -139,9 +170,9 @@ async function exportCragGroups (): Promise<void> {

async function onDBConnected (): Promise<void> {
logger.info('Start exporting crag data as Geojson')
await exportLeafCrags()
await exportCragGroups()
// await exportLeafCrags()
// await exportCragGroups()
await gracefulExit()
}

void connectDB(onDBConnected)
await connectDB(onDBConnected)
94 changes: 94 additions & 0 deletions src/db/utils/jobs/CragGeojson/upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import fs from 'fs'
import { PutObjectCommand, PutObjectCommandOutput, S3Client } from '@aws-sdk/client-s3'
import UploadService, { MapboxUploadCredentials, MapiResponse } from '@mapbox/mapbox-sdk/services/uploads.js'
import { logger } from '../../../../logger.js'
import { WORKING_DIR } from './index.js'

const mapboxUsername = process.env.MAPBOX_USERNAME
const mapboxToken = process.env.MAPBOX_TOKEN

if (mapboxUsername == null) {
throw new Error('MAPBOX_USERNAME not set')
}

if (mapboxToken == null) {
throw new Error('MAPBOX_TOKEN not set')
}

const uploadsClient = UploadService({ accessToken: mapboxToken })

const getCredentials = async (): Promise<MapboxUploadCredentials> => {
return uploadsClient
.createUploadCredentials()
.send()
.then(response => response.body)
}

const stageFileOnS3 = async (credentials: MapboxUploadCredentials, filePath: string): Promise<PutObjectCommandOutput> => {
const s3Client = new S3Client({
region: 'us-east-1',
credentials: {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken
}
})

const command = new PutObjectCommand({
Bucket: credentials.bucket,
Key: credentials.key,
Body: fs.createReadStream(filePath)
})
const res = await s3Client.send(command)
return res
}

interface UploadOptions {
/**
* Tileset unique ID
*/
tilesetId: string
/**
* Tileset name
*/
name: string
/**
* file path to upload
*/
filePath: string
}

const notifyMapbox = async (credentials: MapboxUploadCredentials, { tilesetId, name }: UploadOptions): Promise<MapiResponse> => {
const res = await uploadsClient.createUpload({
tileset: `${mapboxUsername}.${tilesetId}`,
url: credentials.url,
name
}).send()
return res
}

/**
* Upload a tile file to Mapbox.
* @see https://docs.mapbox.com/api/maps/uploads/
* @param options
*/
export const upload = async (options: UploadOptions): Promise<void> => {
try {
const credentials = await getCredentials()
await stageFileOnS3(credentials, options.filePath)
logger.info('File staged on S3')
const res = await notifyMapbox(credentials, options)
if ((res.statusCode >= 200 && res.statusCode < 300)) {
logger.info('File uploaded to Mapbox')
return await Promise.resolve()
}
throw new Error(`Create upload failed with status code ${res.statusCode as string}`)
} catch (err) {
logger.error(err)
}
}

export const uploadCragsTiles = async (): Promise<void> => {
const filePath = `${WORKING_DIR}/crags.mbtiles`
await upload({ tilesetId: 'crags', name: 'all crags and boulders', filePath })
}
2 changes: 1 addition & 1 deletion src/graphql/media/MediaResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const MediaResolvers = {

EntityTag: {
id: (node: EntityTag) => node._id,
targetId: (node: EntityTag) => node.targetId.toUUID().toString(),
targetId: (node: EntityTag) => node.targetId,
lat: (node: EntityTag) => geojsonPointToLatitude(node.lnglat),
lng: (node: EntityTag) => geojsonPointToLongitude(node.lnglat)
},
Expand Down
5 changes: 2 additions & 3 deletions src/model/MediaDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,9 @@ export default class MediaDataSource extends MongoDataSource<MediaObject> {
* @returns `UserMediaWithTags` array
*/
async findMediaByAreaId (areaId: MUUID, ancestors: string): Promise<MediaObject[]> {
const rs = await this.mediaObjectModel.find({
return await this.mediaObjectModel.find({
'entityTags.ancestors': { $regex: areaId.toUUID().toString() }
}).lean()
return rs
})
}

/**
Expand Down
Loading

0 comments on commit 1808d70

Please sign in to comment.