diff --git a/README.md b/README.md index 1ad81d7..f1f93ea 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Mudlet Map Renderer -Mudlet map rendering library. Can be used in Node.js and in browser. +Mudlet map rendering library. Can be used in Node.js and in browser. Until version `1.0.0` API is subject of change. Use with caution! @@ -14,7 +14,7 @@ Until version `1.0.0` API is subject of change. Use with caution! - [ ] Align model with mudlet-map-binary-reader -## Very basic example +## Very basic example ```js const fs = require("fs"); @@ -35,8 +35,15 @@ let settings = { scale: scale } let renderer = new Renderer(null, reader, area, reader.getColors(), settings); fs.writeFileSync("mapFull.svg", renderer.exportSvg()); fs.writeFileSync("mapFragment.svg", renderer.exportSvg(roomId, 10)); -console.log("Map generated"); -``` +console.log("Map generated"); +``` + +## Pathfinding + +The library includes a simple `PathFinder` that searches for routes between +rooms. The path finder now builds route graphs on demand and caches both the +graphs and calculated paths. When two rooms reside in the same area only that +area's graph is used, which speeds up path calculations for large maps. ## Settings and their default values ```js diff --git a/map-fragment/reader/PathFinder.js b/map-fragment/reader/PathFinder.js index 4ee9025..93e6f23 100644 --- a/map-fragment/reader/PathFinder.js +++ b/map-fragment/reader/PathFinder.js @@ -1,26 +1,83 @@ const { MapReader } = require("./MapReader"); -const Graph = require("node-dijkstra") +const Graph = require("node-dijkstra"); class PathFinder { - /** - * - * @param {MapReader} reader + * @param {MapReader} reader */ constructor(reader) { - this.route = new Graph(); - reader.getAreas().forEach(area => area.rooms.forEach(room => { - let exits = Object.values(room.exits).concat(Object.values(room.specialExits)).map(item => [item, room.weight ?? 1]); - this.route.addNode(room.id.toString(), Object.fromEntries(exits)) - })) + this.reader = reader; + this.areaGraphs = new Map(); + this.pathCache = new Map(); } - path(from, to) { - return this.route.path(from.toString(), to.toString()) + /** + * Build and cache a graph for a specific area. + * @param {number} areaId + * @returns {Graph} + */ + _buildAreaGraph(areaId) { + const graph = new Graph(); + const area = this.reader.getAreaProperties(areaId); + if (area?.rooms) { + area.rooms.forEach((room) => { + const exits = Object.values(room.exits) + .concat(Object.values(room.specialExits)) + .map((id) => [id, room.weight ?? 1]); + graph.addNode(room.id.toString(), Object.fromEntries(exits)); + }); + } + this.areaGraphs.set(areaId, graph); + return graph; + } + + /** + * Lazily build a graph for the whole map. + * @returns {Graph} + */ + _buildGlobalGraph() { + const graph = new Graph(); + this.reader.getAreas().forEach((area) => + area.rooms.forEach((room) => { + const exits = Object.values(room.exits) + .concat(Object.values(room.specialExits)) + .map((id) => [id, room.weight ?? 1]); + graph.addNode(room.id.toString(), Object.fromEntries(exits)); + }) + ); + this.globalGraph = graph; + return graph; } + /** + * Find a path between two rooms. Results are cached. + * @param {number} from + * @param {number} to + * @returns {Array|null} + */ + path(from, to) { + const cacheKey = `${from}-${to}`; + if (this.pathCache.has(cacheKey)) { + return this.pathCache.get(cacheKey); + } + + const fromRoom = this.reader.getRoomById(from); + const toRoom = this.reader.getRoomById(to); + + let graph; + if (fromRoom && toRoom && fromRoom.areaId === toRoom.areaId) { + const areaId = fromRoom.areaId; + graph = this.areaGraphs.get(areaId) ?? this._buildAreaGraph(areaId); + } else { + graph = this.globalGraph ?? this._buildGlobalGraph(); + } + + const result = graph.path(from.toString(), to.toString()); + this.pathCache.set(cacheKey, result); + return result; + } } module.exports = { - PathFinder -} \ No newline at end of file + PathFinder, +};