diff --git a/package-lock.json b/package-lock.json index 9ffcb01..6fd8737 100644 --- a/package-lock.json +++ b/package-lock.json @@ -367,8 +367,7 @@ "bluebird": { "version": "3.5.1", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", - "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==", - "dev": true + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" }, "brace-expansion": { "version": "1.1.11", diff --git a/package.json b/package.json index c884213..866652a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "index.js", "scripts": { "test": "npm run test:lint && npm run test:unit", - "test:unit": "tap -- ts_test/*.ts", + "test:unit": "tap -- ts_test/*.ts ts_test/**/*.ts", "test:lint": "tslint -c .tslintrc.json ts/**/*.ts && standard", "build:types-js": "pbjs -t static-module -w commonjs -l \"eslint-disable one-var, no-mixed-operators\" --es6 spec/*.proto -o ts/Types.js && standard --fix ts/Types.js", "build:types-ts": "npm run build:types-js && pbts ts/Types.js -o ts/Types.d.ts && tslint -c .tslintrc.json --fix ts/Types.d.ts", @@ -48,6 +48,7 @@ "typescript-eslint-parser": "^16.0.1" }, "dependencies": { + "bluebird": "^3.5.1", "long": "^4.0.0", "protobufjs": "^6.8.6", "three": "^0.94.0", diff --git a/ts/api/FeatureType.ts b/ts/api/FeatureType.ts index 5b3e8b6..5a48f9b 100644 --- a/ts/api/FeatureType.ts +++ b/ts/api/FeatureType.ts @@ -29,3 +29,36 @@ enum FeatureType { } export default FeatureType + +export function parseFeatureString (type: string): FeatureType { + switch (type) { + case 'bool': return FeatureType.bool + case 'bytes': return FeatureType.bytes + case 'double': return FeatureType.double + case 'fixedint16': return FeatureType.fixedint16 + case 'fixedint32': return FeatureType.fixedint32 + case 'fixedint64': return FeatureType.fixedint64 + case 'fixedint8': return FeatureType.fixedint8 + case 'float': return FeatureType.float + case 'int16': return FeatureType.int16 + case 'int32': return FeatureType.int32 + case 'int64': return FeatureType.int64 + case 'int8': return FeatureType.int8 + case 'sfixedint16': return FeatureType.sfixedint16 + case 'sfixedint32': return FeatureType.sfixedint32 + case 'sfixedint64': return FeatureType.sfixedint64 + case 'sfixedint8': return FeatureType.sfixedint8 + case 'sint16': return FeatureType.sint16 + case 'sint32': return FeatureType.sint32 + case 'sint64': return FeatureType.sint64 + case 'sint8': return FeatureType.sint8 + case 'string': return FeatureType.string + case 'uint16': return FeatureType.uint16 + case 'uint32': return FeatureType.uint32 + case 'uint64': return FeatureType.uint64 + case 'uint8': return FeatureType.uint8 + case 'varbytes': return FeatureType.varbytes + case 'varstring': return FeatureType.varstring + } + throw new Error(`Unknown feature type ${type}`) +} diff --git a/ts/api/INode.ts b/ts/api/INode.ts index d4b3abd..19e682a 100644 --- a/ts/api/INode.ts +++ b/ts/api/INode.ts @@ -4,4 +4,5 @@ import IBox3 from './IBox3' export default interface INode extends INodeSelector { readonly numPoints: (number | Long) readonly bounds?: IBox3 + readonly spacing?: number } diff --git a/ts/format/IInput.ts b/ts/format/IInput.ts new file mode 100644 index 0000000..8ead052 --- /dev/null +++ b/ts/format/IInput.ts @@ -0,0 +1,4 @@ +export default interface IInput { + id (): string + loadJson (filename: string): Promise +} diff --git a/ts/format/ept/index.ts b/ts/format/ept/index.ts new file mode 100644 index 0000000..2f0a718 --- /dev/null +++ b/ts/format/ept/index.ts @@ -0,0 +1,104 @@ +import IPNextIO from '../../api/IPNextIO' +import ITreeQuery from '../../api/ITreeQuery' +import INodeQuery from '../../api/INodeQuery' +import IPointQuery from '../../api/IPointQuery' +import INode from '../../api/INode' +import ITree from '../../api/ITree' +import IFeature from '../../api/IFeature' +import AbstractIO from '../../util/AbstractIO' +import { Stream, ReadableStream } from 'ts-stream' +import selectNodes from '../../util/selectNodes' +import IInput from '../IInput' +import INodeTree from '../../util/INodeTree' +import loadTree from './tree' +import loadRootNodes from './nodes' +import INodeSelector from '../../api/INodeSelector' +import IReader from '../../reader/IReader' + +function createBinReader (schema: IFeature[], request: IFeature[]): IReader { + +} + +function createReader (dataType: string, schema: IFeature[]): IReader { + if (dataType === 'bin') { + return createBinReader(schema) + } + throw new Error(`Unsupported dataType: ${dataType}`) +} + +export class EPT extends AbstractIO implements IPNextIO { + + tree: Promise + input: IInput + rootNodes: Promise + + constructor (input: IInput) { + super() + this.input = input + } + + getTrees (query?: ITreeQuery): ReadableStream { + return Stream.from([this.loadTree()]) + } + + loadTree (): Promise { + if (!this.tree) { + this.tree = loadTree(this.input) + } + return this.tree + } + + loadReader (): Promise { + return this.loadTree() + .then((tree: ITree) => createReader(tree.metadata.dataType, tree.schema)) + } + + getNodes (query?: INodeQuery): ReadableStream { + const stream = new Stream() + this.loadRootNodes().then(rootTreeNodes => { + selectNodes(query, rootTreeNodes, { + addNode (node: INode): void { + stream.write(node) + }, + isClosed: (): boolean => stream.isEnded(), + end () { + stream.end() + } + }) + }) + return stream + } + + loadRootNodes (): Promise { + if (!this.rootNodes) { + this.rootNodes = this.loadTree() + .then((tree: ITree) => loadRootNodes(this.input, tree)) + } + return this.rootNodes + } + + loadNodes (selectors?: INodeSelector[]): ReadableStream { + if (selectors) { + // TODO: Implement me! + } + return this.getNodes() + } + + getNodePoints (node: INode, treeFeatures: IReader, output: Stream<{ [k: string]: any }>): void { + this.input.binaryStream(`${node.id}`) + } + + getPoints (query?: IPointQuery): ReadableStream<{ [k: string]: any }> { + const pointStream = new Stream<{ [k: string]: any }>() + this.loadReader() + .then(reader => { + const nodeStream = this.loadNodes(query.nodes) + nodeStream.forEach(node => + this.getNodePoints(node, reader, pointStream) + ).then(() => { + pointStream.end() + }) + }) + return pointStream + } +} diff --git a/ts/format/ept/nodes.ts b/ts/format/ept/nodes.ts new file mode 100644 index 0000000..bef45d8 --- /dev/null +++ b/ts/format/ept/nodes.ts @@ -0,0 +1,104 @@ +import { slippyToString, SlippyMap, parseSlippy, slippyParentString, slippyBounds, slippySphere, SLIPPY_ZERO } from '../../util/slippy' +import INodeTree from '../../util/INodeTree' +import ITree from '../../api/ITree' +import IInput from '../IInput' +import IBox3 from '../../api/IBox3' +import dot from '../../util/dot' +import addToPoint from '../../util/addToPoint' +import differencePointPoint from '../../util/differencePointPoint' +import multiplyWithPoint from '../../util/multiplyWithPoint' +import IVector3 from '../../api/IVector3' +import ISphere from '../../util/ISphere' +import distancePointPoint from '../../util/distancePointPoint' + +function nullOp (): null { + return null +} + +interface INodeInfo { + size: IVector3 + offset: IVector3 + sphereRadius: number + hierarchyStep: number + ticks: number +} + +function scaleAndOffset (point: IVector3, nodeInfo: INodeInfo): IVector3 { + return addToPoint(multiplyWithPoint(point, nodeInfo.size), nodeInfo.offset) +} + +function eptSpacing (loc: SlippyMap, nodeInfo: INodeInfo): number { + +} + +function eptBounds (loc: SlippyMap, nodeInfo: INodeInfo): IBox3 { + const bounds = slippyBounds(loc) + scaleAndOffset(bounds.min, nodeInfo) + scaleAndOffset(bounds.max, nodeInfo) + return bounds +} + +function eptSphere (loc: SlippyMap, nodeInfo: INodeInfo): ISphere { + const sphere = slippySphere(loc) + sphere.radius *= nodeInfo.sphereRadius + scaleAndOffset(sphere.center, nodeInfo) + return sphere +} + +async function loadNodes (input: IInput, parent: SlippyMap, parentId: string, nodeInfo: INodeInfo): Promise { + const id = slippyToString(parent) + const byParent: { [k: string]: (INodeTree[] | undefined) } = {} + const children: INodeTree[] = [] + byParent[parentId] = children + const dmax: number = parent.d + nodeInfo.hierarchyStep + const counts: { [k: string]: number } = await input.loadJson(`${id}.json`) + for (const childId in counts) { + const numPoints = counts[childId] + const loc = parseSlippy(childId) + const parentId = slippyParentString(loc) + let siblings = byParent[parentId] + if (siblings === undefined) { + siblings = [] + byParent[parentId] = siblings + } + const nodeTree: INodeTree = { + node: { + numPoints, + bounds: eptBounds(loc, nodeInfo), + spacing: eptSpacing(loc, nodeInfo) + }, + boundingSphere: eptSphere(loc, nodeInfo), + children: nullOp + } + if (loc.d === dmax) { + let subChildren: Promise + nodeTree.children = (): Promise => { + if (!subChildren) { + subChildren = loadNodes(input, loc, childId, nodeInfo) + } + return subChildren + } + } + siblings.push(nodeTree) + } + return children +} + +export default function loadRootNodes (input: IInput, tree: ITree): Promise { + // There is no deeper hierarchy step + let hierarchyStep = Infinity + if (tree.metadata && !isNaN(tree.metadata.hierarchyStep)) { + hierarchyStep = tree.metadata.hierarchyStep + } + if (tree.metadata && tree.metadata.hierarchyType && tree.metadata.hierarchyType !== 'json') { + throw new Error('Only supported hierarchy type is json') + } + const nodeInfo = { + size: differencePointPoint(tree.bounds.max, tree.bounds.min), + sphereRadius: distancePointPoint(tree.bounds.max, tree.bounds.min), + offset: tree.bounds.min, + hierarchyStep, + ticks: tree.metadata.ticks + } + return loadNodes(input, SLIPPY_ZERO, '0-0-0-0', nodeInfo) +} diff --git a/ts/format/ept/tree.ts b/ts/format/ept/tree.ts new file mode 100644 index 0000000..0804961 --- /dev/null +++ b/ts/format/ept/tree.ts @@ -0,0 +1,54 @@ +import IBox3 from '../../api/IBox3' +import IVector3 from '../../api/IVector3' +import { parseFeatureString } from '../../api/FeatureType' +import IFeature from '../../api/IFeature' +import Feature from '../../api/Feature' +import ITree from '../../api/ITree' +import IInput from '../IInput' + +function parseFeature (raw: {name: string, type: string }): IFeature { + const type = parseFeatureString(raw.type) + for (const featName in Feature) { + const feat = Feature[featName] + if (feat.name === raw.name && feat.type === type) { + return feat + } + } + return { + name: raw.name, + type + } +} + +function parseBounds (raw: number[]): IBox3 { + return { + min: parseVector(raw, 0), + max: parseVector(raw, 3) + } +} + +function parseVector (raw: number[], offset: number): IVector3 { + return { x: raw[offset], y: raw[offset + 1], z: raw[offset + 2] } +} + +export default async function loadTree (input: IInput): Promise { + const json = await input.loadJson('entwine.json') + return { + id: input.id(), + bounds: parseBounds(json.bounds), + boundsConforming: parseBounds(json.boundsConforming), + scale: json.scale, + offset: parseVector(json.offset, 0), + numPoints: json.numPoints, + schema: json.schema.map(parseFeature), + metadata: { + ...json.metadata, + dataType: json.dataType, + hierarchyStep: json.hierarchyStep, + hierarchyType: json.hierarchyType, + reprojection: json.reprojection, + srs: json.srs, + ticks: json.ticks + } + } +} diff --git a/ts/util/INodeTree.ts b/ts/util/INodeTree.ts index 4983a05..6040e62 100644 --- a/ts/util/INodeTree.ts +++ b/ts/util/INodeTree.ts @@ -1,8 +1,8 @@ import INode from '../api/INode' -import { Sphere } from 'three' +import ISphere from './ISphere' export default interface INodeTree { node: INode, - boundingSphere: Sphere, - children: INodeTree[] + boundingSphere: ISphere, + children (): PromiseLike | null } diff --git a/ts/util/addToPoint.ts b/ts/util/addToPoint.ts new file mode 100644 index 0000000..6b1d55e --- /dev/null +++ b/ts/util/addToPoint.ts @@ -0,0 +1,8 @@ +import IVector3 from '../api/IVector3' + +export default function addToPoint (toPoint: IVector3, add: IVector3): IVector3 { + toPoint.x += add.x + toPoint.y += add.y + toPoint.z += add.z + return toPoint +} diff --git a/ts/util/differencePointPoint.ts b/ts/util/differencePointPoint.ts new file mode 100644 index 0000000..cbce4b8 --- /dev/null +++ b/ts/util/differencePointPoint.ts @@ -0,0 +1,9 @@ +import IVector3 from '../api/IVector3' + +export default function distancePointPoint (a: IVector3, b: IVector3): IVector3 { + return { + x: a.x - b.x, + y: a.y - b.y, + z: a.z - b.z + } +} diff --git a/ts/util/multiplyWithPoint.ts b/ts/util/multiplyWithPoint.ts new file mode 100644 index 0000000..67c0711 --- /dev/null +++ b/ts/util/multiplyWithPoint.ts @@ -0,0 +1,8 @@ +import IVector3 from '../api/IVector3' + +export default function multiplyWithPoint (toPoint: IVector3, multiply: IVector3): IVector3 { + toPoint.x *= multiply.x + toPoint.y *= multiply.y + toPoint.z *= multiply.z + return toPoint +} diff --git a/ts/util/selectNodes.ts b/ts/util/selectNodes.ts index e3139b2..dde234c 100644 --- a/ts/util/selectNodes.ts +++ b/ts/util/selectNodes.ts @@ -16,6 +16,7 @@ import IDisplay from '../api/IDisplay' import ILongRange from '../api/ILongRange' import Long from 'long' import IDensityRange from '../api/IDensityRange' +import bb from 'bluebird' function getPerspectiveCamera (input: IPerspectiveCamera): PerspectiveCamera { if (input instanceof PerspectiveCamera) { @@ -181,8 +182,14 @@ function filterAndSortByWeight (nodeList: INodeTree[], fDisplays: IFrustumDispla .map(weightedNode => weightedNode.node) } -function allChildren (nodeList: INodeTree[]): INodeTree[] | null { - const childrenList = nodeList.map(node => node.children).filter(Boolean) +async function allChildren (nodeList: INodeTree[]): Promise { + const childrenList = await bb.map( + nodeList, + (node: INodeTree) => node.children(), + { + concurrency: 15 + } + ).filter(Boolean) if (childrenList.length === 0) { return null } @@ -228,7 +235,7 @@ export default async function selectNodes (query: INodeQuery, treeNodeList: INod } output.addNode(node.node) } - treeNodeList = allChildren(treeNodeList) + treeNodeList = await allChildren(treeNodeList) } output.end() return true diff --git a/ts/util/slippy.ts b/ts/util/slippy.ts new file mode 100644 index 0000000..a46301f --- /dev/null +++ b/ts/util/slippy.ts @@ -0,0 +1,105 @@ +import IBox3 from '../api/IBox3' +import IVector3 from '../api/IVector3' +import ISphere from './ISphere' +import { ZERO } from 'long' +import distancePointPoint from './distancePointPoint' + +export interface SlippyMap { + d: number, + x: number, + y: number, + z: number +} + +export function slippyToString (loc: SlippyMap): string { + return `${loc.d}-${loc.x}-${loc.y}-${loc.z}` +} + +const ZERO_BOUNDS: IBox3 = { + min: { x: 0, y: 0, z: 0 }, + max: { x: 1, y: 1, z: 1 } +} + +const D_BASE: { [ k: number ]: number | undefined } = {} +function getBase (d: number): number { + let base = D_BASE[d] + if (base === undefined) { + base = 1 / Math.pow(2, d) + D_BASE[d] = base + } + return base +} +const D_RADIUS: { [ k: number ]: number | undefined } = {} +function getRadius (base: number): number { + let radius = D_RADIUS[base] + if (radius === undefined) { + radius = distancePointPoint( + { x: 0, y: 0, z: 0 }, + { x: base, y: base, z: base } + ) * 0.5 + D_RADIUS[base] = radius + } + return radius +} + +export function slippyBounds (loc: SlippyMap): IBox3 { + if (loc.d <= 0) { + return ZERO_BOUNDS + } + const base = getBase(loc.d) + const min = { + x: loc.x * base, + y: loc.y * base, + z: loc.z * base + } + return { + min, + max: { + x: min.x + base, + y: min.y + base, + z: min.z + base + } + } +} + +export function slippySphere (loc: SlippyMap): ISphere { + const base = getBase(loc.d) + return { + radius: getRadius(base), + center: { + x: (loc.x + 0.5) * base, + y: (loc.y + 0.5) * base, + z: (loc.z + 0.5) * base + } + } +} + +export const SLIPPY_ZERO: SlippyMap = { d: 0, x: 0, y: 0, z: 0 } + +const PARSER = /^(\d+)-(\d+)-(\d+)-(\d+)$/i + +export function parseSlippy (from: string): SlippyMap { + const parts = PARSER.exec(from) + if (parts === null) { + throw new Error(`Invalid slippMap ID: ${from}`) + } + return { + d: parseInt(parts[1], 10), + x: parseInt(parts[2], 10), + y: parseInt(parts[3], 10), + z: parseInt(parts[4], 10) + } +} + +export function slippyParent (map: SlippyMap): SlippyMap { + return { + d: map.d - 1, + x: map.x / 2 | 0, + y: map.y / 2 | 0, + z: map.z / 2 | 0 + } +} + +export function slippyParentString (map: SlippyMap): string { + return slippyToString(slippyParent(map)) +} diff --git a/ts_test/ept/nodes.ts b/ts_test/ept/nodes.ts new file mode 100755 index 0000000..1bf3c54 --- /dev/null +++ b/ts_test/ept/nodes.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env node --require ts-node/register +import { test } from 'tap' +import loadRootNodes from '../../ts/format/ept/nodes' +import IInput from '../../ts/format/IInput' +import FeatureType from '../../ts/api/FeatureType' +import Feature from '../../ts/api/Feature' +import ITree from '../../ts/api/ITree' + +test('Basic nodes loading test', async t => { + const fileOrder = [ + '0-0-0-0.json' + ] + const input = { + id () { return 'abcd' }, + async loadJson (filename: string): Promise { + if (fileOrder.length === 0) { + t.fail(`Loading ${filename} even though it wasn't expected`) + } + const next = fileOrder.shift() + if (next !== filename) { + t.fail(`Loading order ${filename} was requested but ${next}`) + } + return { + '0-0-0-0': 12, + '1-0-0-0': 10, + '1-0-1-0': 2 + } + } + } + const tree: ITree = { + id: 'abcd', + bounds: { min: { x: 1, y: 2, z: 3 }, max: { x: 4, y: 5, z: 6 } }, + numPoints: 100, + schema: [], + metadata: { + hierarchyStep: 5 + } + } + const nodes = await loadRootNodes(input, tree) + t.deepEquals(nodes[0].node.bounds, { + min: { x: 1, y: 2, z: 3 }, + max: { x: 2.5, y: 3.5, z: 4.5 } + }) +}) diff --git a/ts_test/ept/tree.ts b/ts_test/ept/tree.ts new file mode 100755 index 0000000..c661471 --- /dev/null +++ b/ts_test/ept/tree.ts @@ -0,0 +1,63 @@ +#!/usr/bin/env node --require ts-node/register +import { test } from 'tap' +import loadTree from '../../ts/format/ept/tree' +import IInput from '../../ts/format/IInput' +import FeatureType from '../../ts/api/FeatureType' +import Feature from '../../ts/api/Feature' + +test('loadTree', async t => { + const testInput: IInput = { + id: (): string => 'abcd', + loadJson: async (filename: string): Promise => { + t.equals(filename, 'entwine.json', 'root filename is entwine.json') + return { + bounds: [0, 0, 0, 1, 1, 1], + boundsConforming: [0.2, 0.2, 0.2, 0.8, 0.8, 0.8], + scale: 0.5, + offset: [4, 5, 6], + numPoints: 10, + schema: [ + { type: 'uint32', name: 'fancy' }, + { type: 'double', name: 'x' } + ], + hierarchyStep: 5, + hierarchyType: 'json', + reprojection: 'raspy', + dataType: 'laszip', + srs: 'fancysrs', + ticks: 5, + metadata: { + fancy: true + } + } + } + } + const tree = await loadTree(testInput) + t.deepEquals(tree, { + id: 'abcd', + bounds: { + min: { x: 0, y: 0, z: 0 }, + max: { x: 1, y: 1, z: 1 } + }, + boundsConforming: { + min: { x: 0.2, y: 0.2, z: 0.2 }, + max: { x: 0.8, y: 0.8, z: 0.8 } + }, + scale: 0.5, + offset: { x: 4, y: 5, z: 6 }, + numPoints: 10, + schema: [ + { type: FeatureType.uint32, name: 'fancy' }, + Feature.x + ], + metadata: { + fancy: true, + reprojection: 'raspy', + dataType: 'laszip', + srs: 'fancysrs', + ticks: 5, + hierarchyStep: 5, + hierarchyType: 'json' + } + }) +}) diff --git a/ts_test/selectNodes.ts b/ts_test/selectNodes.ts index 22528b3..be72d8e 100755 --- a/ts_test/selectNodes.ts +++ b/ts_test/selectNodes.ts @@ -27,7 +27,7 @@ function createTreeNode (node: INode): INodeTree { return { node, boundingSphere: getBoundingSphere(node), - children: null + children: () => null } } @@ -47,13 +47,6 @@ function gatherNodes (output: INode[]) { } } -function box3 (a: IVector3, b: IVector3): Box3 { - return new Box3( - new Vector3(a.x, a.y, a.z), - new Vector3(b.x, b.y, b.z) - ) -} - test('select everything on an empty list should just end', async t => { let ended = false const done = await selectNodes({}, [], { @@ -139,7 +132,7 @@ test('Only points in the cut should be returned', async t => { const { a, b, c, d, e } = createABNodes() const output = await selectInABNodes([e, d, c, b, a], { cut: [ - box3({ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 }) + { min: { x: 0, y: 0, z: 0 }, max: { x: 1, y: 1, z: 1 } } ] }) t.deepEquals(output, [b, a]) @@ -149,8 +142,8 @@ test('With multiple cuts, nodes that cut any of those should be returned', async const { a, b, c, d, e } = createABNodes() const output = await selectInABNodes([e, d, c, b, a], { cut: [ - box3({ x: .0, y: .0, z: .0 }, { x: 1.0, y: 1.0, z: 1.0 }), - box3({ x: 9.0, y: 9.0, z: 9.0 }, { x: 10.0, y: 10.0, z: 10.0 }) + { min: { x: .0, y: .0, z: .0 }, max: { x: 1.0, y: 1.0, z: 1.0 } }, + { min: { x: 9.0, y: 9.0, z: 9.0 }, max: { x: 10.0, y: 10.0, z: 10.0 } } ] }) t.deepEquals(output, [e, d, b, a]) diff --git a/ts_test/slippy.ts b/ts_test/slippy.ts new file mode 100755 index 0000000..055f6c7 --- /dev/null +++ b/ts_test/slippy.ts @@ -0,0 +1,102 @@ +#!/usr/bin/env node --require ts-node/register +import { test } from 'tap' +import IBox3 from '../ts/api/IBox3' +import { parseSlippy, slippyToString, slippyBounds, SLIPPY_ZERO, slippyParent, slippySphere } from '../ts/util/slippy' +import ISphere from '../ts/util/ISphere' + +test('parseSlippy', async t => { + + t.deepEquals( + parseSlippy('0-0-0-0'), + { d: 0, x: 0, y: 0, z: 0 } + , 'simple parser') + + t.deepEquals( + parseSlippy('1-2-3-4'), + { d: 1, x: 2, y: 3, z: 4 }, + 'each dimension is mapped properly' + ) + + t.deepEquals( + parseSlippy('99-999-9999-99999'), + { d: 99, x: 999, y: 9999, z: 99999 }, + 'dimension number is irrelevant' + ) + + try { + parseSlippy('x-x-x-x') + } catch (e) { + return + } + t.fail('invalid id should throw an error') +}) + +test('slippyBounds', async t => { + const boundsTestData: { [ k: string ]: IBox3 } = { + '0-0-0-0': { + min: { x: 0, y: 0, z: 0 }, + max: { x: 1, y: 1, z: 1 } + }, + '1-0-0-0': { + min: { x: 0, y: 0, z: 0 }, + max: { x: 0.5, y: 0.5, z: 0.5 } + }, + '4-1-1-1': { + min: { x: 1 / 16, y: 1 / 16, z: 1 / 16 }, + max: { x: 2 / 16, y: 2 / 16, z: 2 / 16 } + } + } + + for (const slippyId in boundsTestData) { + const slippyMap = parseSlippy(slippyId) + const expectedBounds = boundsTestData[slippyId] + t.deepEquals(slippyBounds(slippyMap), expectedBounds, `bounds for ${slippyId}`) + // Verify cache + t.deepEquals(slippyBounds(slippyMap), expectedBounds, `verifying ${slippyId}`) + } +}) + +test('slippyToString', async t => { + t.equals(slippyToString({ d: 1, x: 2, y: 3, z: 4 }), '1-2-3-4', 'simply stringify') +}) + +test('slippyParent', async t => { + const parentTestData: { [ k: string ]: string } = { + '1-0-0-0': '0-0-0-0', + '1-1-0-0': '0-0-0-0', + '1-0-1-0': '0-0-0-0', + '1-0-0-1': '0-0-0-0', + '1-1-1-1': '0-0-0-0', + '2-0-0-0': '1-0-0-0', + '2-1-1-1': '1-0-0-0', + '2-2-2-2': '1-1-1-1', + '2-3-3-3': '1-1-1-1', + '3-1-3-7': '2-0-1-3', + '66-22-12-43': '65-11-6-21' + } + for (const childId in parentTestData) { + const parentId = parentTestData[childId] + t.deepEquals(slippyParent(parseSlippy(childId)), parseSlippy(parentId), `parent of ${childId}`) + } +}) + +test('slippySphere', async t => { + const sphereTestData: { [ k: string ]: ISphere } = { + '0-0-0-0': { + radius: 0.8660254037844386, + center: { x: 0.5, y: 0.5, z: 0.5 } + }, + '1-0-0-0': { + radius: 0.4330127018922193, + center: { x: 0.25, y: 0.25, z: 0.25 } + }, + '4-1-2-3': { + radius: 0.05412658773652741, + center: { x: 0.09375, y: 0.15625, z: 0.21875 } + } + } + for (const slippyId in sphereTestData) { + const expectedSphere = sphereTestData[slippyId] + t.deepEquals(slippySphere(parseSlippy(slippyId)), expectedSphere, `sphere of ${slippyId}`) + } +})