import { Vector3, Box3 } from "../geometry.js";
import { EntityRef, NodeRef, EdgeRef, DirEdgeRef } from "./entity.js";
import { HookContainer } from "../hook_container.js";
import { asyncFrom } from "../utils.js";
import { NodeTypeRegistry } from "../node_type.js";
import { LayerRegistry } from "../layer.js";
/** Abstract mapper backend, i.e. what map is being presented.
* The backend translates between the concept of a map and a database, a file, an API, or whatever else is actually being used to store the data.
* Most methods here are low-level; users of the backend should use methods from EntityRef and its children which delegate to the MapBackend.
*
* Underlying structure:
* The backend consists of a set of entities, which can have arbitrary properties.
* A special entity, "global", is used for properties of the whole map.
* Two types of entities have specific handling to form a graph:
* "node" - an entity with a parent and positional information.
* "edge" - an entity connecting two adjacent nodes.
*/
class MapBackend {
constructor() {
this.loaded = false;
this.hooks = new HookContainer();
this.nodeTypeRegistry = new NodeTypeRegistry();
this.layerRegistry = new LayerRegistry();
this.entityCache = {};
}
getEntityCache(id) {
let cache = this.entityCache[id];
if(cache === undefined) {
cache = this.entityCache[id] = {
properties: {},
};
}
return cache;
}
/** Get the database version number. Implementation defined.
* @returns {number}
*/
getVersionNumber() {
throw "getVersionNumber not defined";
}
/** Get the latest backend version number. Implementation defined. Must be greater than zero.
* @returns {number}
*/
getBackendVersionNumber() {
throw "getBackendVersionNumber not defined";
}
/** Get a number property on an entity.
* Has a default implementation based on string properties.
* @returns {number}
*/
async getPNumber(entityId, propertyName) {
return parseFloat(await this.getPString(entityId, propertyName));
}
/** Set a number property on an entity.
* Has a default implementation based on string properties.
*/
async setPNumber(entityId, propertyName, value) {
return this.setPString(entityId, propertyName, value.toString());
}
/** Set a Vector3 property on an entity.
* Has a default implementation based on string properties.
*/
async setPVector3(entityId, propertyName, v) {
return this.setPString(entityId, propertyName, JSON.stringify(v));
}
/** Get a Vector3 property on an entity.
* Has a default implementation based on string properties.
* @returns {Vector3}
*/
async getPVector3(entityId, propertyName) {
const object = JSON.parse(await this.getPString(entityId, propertyName));
return Vector3.fromJSON(object.x, object.y, object.z);
}
/** Get a string property on an entity.
* @returns {string}
*/
async getPString(entityId, propertyName) {
entityId;
propertyName;
throw "getPString not implemented";
}
/** Set a string property on an entity. */
async setPString(entityId, propertyName, value) {
entityId;
propertyName;
value;
throw "setPString not implemented";
}
/** Create a new entity in the backend.
* @param type {string} Type of the new entity.
* @returns {EntityRef}
*/
async createEntity(type) {
type;
throw "createEntity not implemented";
}
/** Creates a new "node" entity.
* @param parentId {number|undefined} ID of the parent node, or undefined if the node has no parent.
* @param nodeType {string} Type of the node. "object" or "point".
* @returns {NodeRef}
*/
async createNode(parentId, nodeType) {
parentId;
nodeType;
throw "createNode not implemented";
}
/** Get the parent node of a node by ID, or null if the node has no parent.
* @returns {NodeRef|null}
*/
async getNodeParent(nodeId) {
nodeId;
throw "getNodeParent not implemented";
}
async setNodeParent(nodeId, parentId) {
nodeId;
parentId;
throw "setNodeParent not implemented";
}
/** Get all direct children of a node.
* @returns {AsyncIterable.<NodeRef>}
*/
async getNodeChildren(nodeId) {
nodeId;
throw "getNodeChildren not implemented";
}
/** Check if a node has any children.
* Has a default implementation based on #getNodeChildren().
* @returns {boolean}
*/
async nodeHasChildren(nodeId) {
for await(const child of this.getNodeChildren(nodeId)) {
child;
return true;
}
return false;
}
/** Get a node's type.
* @returns {string}
*/
async getNodeType(nodeId) {
nodeId;
throw "getNodeType not implemented";
}
/** Create a new edge between two nodes.
* Order of node IDs does not matter.
* @param nodeAId {number} The ID of one of the nodes on the edge.
* @param nodeBId {number} The ID of the other node on the edge.
* @returns {EdgeRef} A reference to the new edge.
*/
async createEdge(nodeAId, nodeBId) {
nodeAId;
nodeBId;
throw "createEdge not implemented";
}
/** Get all edges attached to a node.
* @returns {AsyncIterable.<EdgeRef>}
*/
async getNodeEdges(nodeId) {
nodeId;
throw "getNodeEdges not implemented";
}
/** Get the two nodes attached to an edge, in no particular order.
* @returns {AsyncIterable.<NodeRef>}
*/
async getEdgeNodes(edgeId) {
edgeId;
throw "getEdgeNodes not implemented";
}
/** Given an edge and one of the nodes on the edge, get the other node on the edge.
* @param edgeId {number}
* @param nodeId {number}
* Has a default implementation based on #getEdgeNodes().
* @returns {NodeRef}
*/
async getEdgeOtherNode(edgeId, nodeId) {
const [nodeA, nodeB] = await asyncFrom(this.getEdgeNodes(edgeId));
return (nodeA.id == nodeId) ? nodeB : nodeA;
}
/** Remove an edge from the backend.
* Has a default implementation that just removes the entity.
*/
async removeEdge(edgeId) {
return this.removeEntity(edgeId);
}
/** Remove a node from the backend.
* Has a default implementation that just removes the entity.
*/
async removeNode(nodeId) {
return this.removeEntity(nodeId);
}
/** Check if an entity exists.
* @returns {boolean}
*/
async entityExists(entityId) {
entityId;
throw "entityExists not implemented";
}
/** Remove an entity from the backend.
* This method should work to remove any entity.
* However, calling code should use #removeEdge() and #removeNode() when applicable instead, for potential optimization purposes.
*/
async removeEntity(entityId) {
entityId;
throw "removeEntity not implemented";
}
/** Flush the backend to storage.
* This may happen automatically, but flush forces it.
* Has a default implementation that does nothing.
*/
async flush() {
}
/** Create an EntityRef to an entity in this backend.
* Use getNodeRef, getEdgeRef, or getDirEdgeRef for greater type-specific functionality if the entity is a node or edge.
*/
getEntityRef(id) {
return new EntityRef(id, this);
}
/** Create a NodeRef to a node in this backend. */
getNodeRef(id) {
return new NodeRef(id, this);
}
/** Create an EdgeRef to an edge in this backend. */
getEdgeRef(id) {
return new EdgeRef(id, this);
}
/** Create a DirEdgeRef to an edge in this backend, starting from the specified node.
* @param id {number} The ID of the edge to get.
* @param startId {number} The ID of a node attached to this edge.
* @returns {DirEdgeRef} Starting from the specified start ID.
*/
getDirEdgeRef(id, startId) {
return new DirEdgeRef(id, startId, this);
}
/** Get all nodes within a spatial box.
* @param box {Box3} The box to find nodes within.
* @returns {AsyncIterable.<NodeRef>}
*/
getNodesInArea(box) {
box;
throw "getNodesInArea not implemented";
}
/** Get all nodes in or near a spatial box (according to their radii).
* @param box {Box3} The box to find nodes within or near.
* @param minRadius {number} The minimum radius of nodes to return.
* @returns {AsyncIterable.<NodeRef>}
*/
getObjectNodesTouchingArea(box, minRadius) {
box;
minRadius;
throw "getObjectNodesTouchingArea not implemented";
}
/** Get the edge between two nodes, if it exists.
* @param nodeAId {number} The ID of one of the nodes on the edge to find.
* @param nodeBId {number} The ID of the other node on the edge to find.
* @returns {EdgeRef}
*/
getEdgeBetween(nodeAId, nodeBId) {
nodeAId;
nodeBId;
throw "getEdgeBetween not implemented";
}
/** Get all nearby nodes within a specified blend distance of the specified node.
* Has a default implementation based on #getNodesInArea().
* @param nodeRef {NodeRef} The node that is the spatial center of the search.
* @param blendDistance {number} How far out to look for nodes? (Necessary to avoid searching the entire map.)
* @returns {AsyncIterable.<NodeRef>} All the discovered nodes. Does not include the original node.
*/
async * getNearbyNodes(nodeRef, blendDistance) {
for await (const otherNodeRef of this.getNodesInArea(Box3.fromRadius(await nodeRef.getCenter(), blendDistance))) {
if(otherNodeRef.id !== nodeRef.id) {
yield otherNodeRef;
}
}
}
/** Get all nodes connected to the specified node by one level of edges (that is, one edge).
* Has a default implementation based on #getNodeEdges().
* @param nodeRef {NodeRef} The node to search for connections on.
* @returns {AsyncIterable.<NodeRef>} The connected nodes.
*/
async * getConnectedNodes(nodeRef) {
for await (const dirEdgeRef of this.getNodeEdges(nodeRef.id)) {
yield await dirEdgeRef.getDirOtherNode();
}
}
/** Get all edges within a specified blend distance that intersect with the given edge.
* Has a default implementation based on #getNodesInArea() and #NodeRef.getEdges().
* @param edgeRef {EdgeRef} The edge to search for intersections on.
* @param blendDistance {number} How far out to search for intersections? (Necessary to avoid searching the entire map.)
* @returns {AsyncIterable.<EdgeRef>} Each intersecting edge found.
*/
async * getIntersectingEdges(edgeRef, blendDistance) {
const line = await edgeRef.getLine();
const distance = Vector3.UNIT.multiplyScalar(blendDistance);
const seen = {
[edgeRef.id]: true,
};
for await (const nodeRef of this.getNodesInArea(new Box3(line.fullMin().subtract(distance), line.fullMax().add(distance)))) {
for await (const dirEdgeRef of nodeRef.getEdges()) {
if(!seen[dirEdgeRef.id]) {
seen[dirEdgeRef.id] = true;
if(line.intersects2(await dirEdgeRef.getLine())) {
yield dirEdgeRef;
}
}
}
}
}
}
export { MapBackend };