Source: geoData.js

function polygonFromQuad(quadObj) {
    return [
        [quadObj.x, quadObj.y],
        [quadObj.x + quadObj.width, quadObj.y],
        [quadObj.x + quadObj.width, quadObj.y + quadObj.height],
        [quadObj.x, quadObj.y + quadObj.height],
        [quadObj.x, quadObj.y]
    ]
}

/**
 * An OSM Node in JSON format,
 * plus a list of Ways that contains it (not contained in the original OSM data).
 * Example:
 * ```json
 * {
 *   "type": "node",    // always "node"
 *   "id": 280525868,   // node ID
 *   "lat": 38.9849002, // node coordinates
 *   "lon": -76.9333648,
 *   "ways": [ ... ]    // integer way IDs
 * }
 * ```
 * @typedef {{
 *  type: string,
 *  id: number,
 *  lat: number,
 *  lon: number,
 *  ways: number[]
 * }} OSMNode
 */

/**
 * An OSM Way in JSON format. Example:
 * ```json
 * {
 *   "type": "way",    // always "way"
 *   "id": 123456,     // integer way ID
 *   "nodes": [ ... ], // integer node IDs
 *   "tags": {         // way OSM tags
 *       "foot": "yes",
 *       "highway": "footway",
 *       "lit": "yes"
 *   }
 * }
 * ```
 * @typedef {{
 *   type: string,
 *   id: number,
 *   nodes: number[],
 *   tags: Object.<string, string>
 * }} OSMWay
 */


/**
 * Contains functions for loading and querying map data.
 * @namespace GeoData
 */
const GeoData = {
    /**
     * Bounding box, used to set bounds of quadtree.
     * @memberof GeoData
     */
    bbox: [[-76.9599, 38.9962], [-76.9295, 38.9795]],
    /**
     * Contains {@link OSMWay}s in JSON format, indexed by ID. 
     * @memberof GeoData
     * @type {Object.<number, OSMWay>}
     */
    ways: {},
    /**
     * Contains the IDs of {@link OSMWay}s contained in {@link GeoData.ways}
     * in a quadtree, indexed by spatial location.
     * 
     * Example query:
     * ```js
     * // Retrieves nodes in quads intersecting a quad of width (0.001, 0.0008) at (long, lat).
     * var quad = {
     *    x: long,
     *    y: lat,
     *    width: 0.001,
     *    height:0.0008
     * };
     * var candidates = GeoData.footpathsQuadtree.retrieve(quad);​
     * ```
     * @memberof GeoData
     */
    nodesQuadtree: undefined,
    /**
     * Contains {@link OSMNode}s in JSON format, indexed by ID.
     * 
     * @memberof GeoData
     * @type {Object.<number, OSMNode>}
     */
    nodes: {},
    untraversableNodes: new Set(),

    /**
     * Gets a dictionary of {@link OSMWay}s in OSM JSON format.
     * @memberof GeoData
     * @returns {Object.<number, OSMWay>}
     */
    getFootpaths: () => this.footpaths,

    /**
     * Clears {@link GeoData.ways}, {@link GeoData.nodes}, and {@link GeoData.nodesQuadtree}.
     * @memberof GeoData
     * @returns {void}
     */
    initFootpaths() {
        this.ways = {};
        this.nodes = {};
        this.nodesQuadtree = new Quadtree({
            x: this.bbox[0][0],
            y: this.bbox[0][1],
            width: this.bbox[1][0] - this.bbox[0][0],
            height: this.bbox[1][1] - this.bbox[0][1]
        }, 10, 10);
    },
    /**
     * Takes OSM JSON as input, and adds footpaths and nodes to
     * {@link GeoData.ways}, {@link GeoData.nodes}, and {@link GeoData.nodesQuadtree}.
     * 
     * Call before addConstruction.
     * @param {Object} json The OSM JSON data in Object format to load.
     */
    addFootpaths(json) {
        console.log("footpaths: Loading " + json.elements.length + " features");
        this.addWays(json, 
            (way) => {
                for (var nodeId of way.nodes) {
                    if (nodeId in this.nodes) {
                        // Add the ways to the node entry.
                        var node = this.nodes[nodeId];
                        var ways = node.ways;

                        if (ways == undefined) {
                            node.ways = [];
                        }
                        node.ways.push(way.id);
                    } else {
                        // Create a new node entry with only the ways,
                        // the rest of it will be filled in later.
                        this.nodes[nodeId] = {
                            ways: [way.id]
                        };
                    }
                }
                return way;
            },
            (node) => {
                var ways = [];
                // If node already has a list of ways, retrieve it.
                if (node.id in this.nodes) {
                    ways = this.nodes[node.id].ways;
                }
                
                node.ways = ways;
                return node;
            });
    },
    /**
     * Takes OSM JSON as input, and adds buildings and nodes to
     * {@link GeoData.ways}, {@link GeoData.nodes}, and {@link GeoData.nodesQuadtree}.
     * 
     * Call before addConstruction.
     * @param {Object} json The OSM JSON data in Object format to load.
     */
    addBuildings(json) {
        console.log("buildings: Loading " + json.elements.length + " features");
        this.addWays(json, 
            (way) => {
                for (var nodeId of way.nodes) {
                    if (nodeId in this.nodes) {
                        // Add the ways to the node entry.
                        var node = this.nodes[nodeId];
                        var ways = node.ways;

                        if (ways == undefined) {
                            node.ways = [];
                        }
                        node.ways.push(way.id);
                    } else {
                        // Create a new node entry with only the ways,
                        // the rest of it will be filled in later.
                        this.nodes[nodeId] = {
                            ways: [way.id]
                        };
                    }
                }
                return way;
            },
            (node) => {
                var ways = [];
                // If node already has a list of ways, retrieve it.
                if (node.id in this.nodes) {
                    ways = this.nodes[node.id].ways;
                }
                
                // If this is an entrance, add the node to the list of entrances in the way.
                if (node.tags != undefined && "entrance" in node.tags) {
                    for (var wayId of ways) {
                        var way = this.ways[wayId];
                        if ("building" in way.tags) {
                            if(way.entrances == undefined) {
                                way.entrances = [];
                            }
                            way.entrances.push(node.id);
                        }
                    }

                    node.ways = ways;
                    return node;
                }

                // Only add entrance nodes to node list
                // ? Maybe add but mark as untraversable, but for now 
                // there's no reason to, so just save memory
                return undefined;
            });
    },
    /**
     * Takes GeoJSON as input and 
     * marks nodes in {@link GeoData.nodes} as untraversable if they overlap.
     * @param {*} json 
     */
    addConstruction(json) {
        console.log("construction: Loading " + json.features.length + " features");
        // this.constructionGeoJSON = json;
        for (var feature of json.features) {
            var bbox = turf.bbox(feature);
            var candidates = this.nodesQuadtree.retrieve({
                x: bbox[0],
                y: bbox[1],
                width: bbox[2] - bbox[0],
                height: bbox[3] - bbox[1]
            });

            for (var candidate of candidates) {
                var candPoint = turf.point([candidate.x, candidate.y]);
                if (turf.booleanPointInPolygon(candPoint, feature)) {
                    GeoData.untraversableNodes.add(candidate.node);
                }
            }
        }
    },
    addWays(json, wayCallback, nodeCallback) {
        var start = new Date();

        var i = 0;
        var features = json['elements'];
        // console.log(json);
        console.log("Loading " + features.length + " features");

        // Add all features (i.e. ways, nodes)
        for (var feature of features) {
            // Add way
            if (feature.type === 'way') {
                feature = wayCallback(feature);
                if(feature == undefined) continue;

                // Just add the way directly to the dictionary
                this.ways[feature.id] = feature;
            } 
            // Add node
            else if (feature.type === 'node') {
                feature = nodeCallback(feature);
                if(feature == undefined) continue;

                // Add the node to the dictionary..
                this.nodes[feature.id] = feature;

                // .. and the quadtree.
                this.nodesQuadtree.insert({
                    x: feature.lon,
                    y: feature.lat,
                    width: 0.0001,
                    height: 0.00008,
                    node: feature.id
                });
            }

            i++;
        }

        console.log("Loaded " + Object.keys(this.nodes).length + " nodes and " 
            + Object.keys(this.ways).length + " ways in " + (new Date() - start) + "ms.");
    },
    drawQuadtree: function (node) {
        var coords = [];
        this.drawQuadtreeRecursive(node, coords);

        var feature = {
            id: 'gpx-route',
            type: 'Feature',
            properties: {},
            geometry: {
                type: 'MultiPolygon',
                coordinates: coords
            }
        };
        console.log("Drew " + coords.length + " rects.");
        Draw.add(feature);
    },
    drawQuadtreeRecursive: function (node, coords, includeObjects) {
        if (node == undefined) return;

        //no subnodes? draw the current node 
        if (node.nodes.length === 0) {
            coords.push([polygonFromQuad(node.bounds)]);

            if (includeObjects) {
                for (var obj of node.objects) {
                    coords.push([polygonFromQuad(obj)]);
                }
            }
            //has subnodes? drawQuadtree them!
        } else {
            for (var i = 0; i < node.nodes.length; i = i + 1) {
                this.drawQuadtreeRecursive(node.nodes[i], coords);
            }
        }
    },
    /**
     * 
     * @param {number[]} point Long/lat coordinates in array form.
     * @returns {Object|undefined} A quad from the nodes quadtree, or undefined if not found.
     * See {@link GeoData.nodesQuadtree} for object layout.
     */
    nearestFootpath(point) {
        var candidates = this.nodesQuadtree.retrieve({
            x: point[0],
            y: point[1],
            width: 0.01,
            height: 0.08
        });

        if (!candidates || candidates.length == 0) {
            return undefined;
        } else {
            var minDist = 999999;
            var minQuad = candidates[0];

            for (var quad of candidates) {
                var dist = Algorithms.getDistance({lon: point[0], lat:point[1]}, GeoData.nodes[quad.node]);
                if (dist < minDist) {
                    minDist = dist;
                    minQuad = quad;
                }
            }
            return minQuad;
        }
    }
};

async function fetchJson(path) {
    return fetch(path).then(response => response.json());
}

// Load GeoData
(async () => {
    GeoData.initFootpaths();
    GeoData.addFootpaths(await fetchJson('./res/footpaths/footpaths.min.json'));
    GeoData.addBuildings(await fetchJson('./res/buildings/buildings.min.json'));
    GeoData.addConstruction(await fetchJson('./res/constructions/construction.min.geojson'));
    // document.getElementById("loading-info-text").innerHTML = "Loaded.";
    // GeoData.addFootpaths(
    //     await fetch('./res/footpaths/footpaths.min.json').then(response => response.json()));
    // GeoData.setFootpathsXml('./res/footpaths.osm');
})();

var showNodeCheckbox = document.getElementById("show-path-nodes-checkbox");
showNodeCheckbox.addEventListener('change', (event) => {
    if (!event.currentTarget.checked) {
        Draw.delete('points');
    }
})
map.on('mousemove', (e) => {
    if (showNodeCheckbox.checked) {

        var quad = {
            x: e.lngLat["lng"] - 0.0005,
            y: e.lngLat['lat'] - 0.0004,
            width: 0.001,
            height: 0.0008
        };

        var candidates = GeoData.nodesQuadtree.retrieve(quad);
        var traversableNodes = turf.multiPoint(candidates
            .filter(i => !GeoData.untraversableNodes.has(i.node))
            .map(i => [i.x, i.y]),
            {},
            { id: 'points' });
        var untraversableNodes = turf.multiPoint(candidates
            .filter(i => GeoData.untraversableNodes.has(i.node))
            .map(i => [i.x, i.y]), {
                red: true
            },
            { id: 'points-red' });

        Draw.add(traversableNodes);
        Draw.add(untraversableNodes);
        console.log(untraversableNodes);
    }

    // var feature2 = {
    //     id: 'points2',
    //     type: 'Feature',
    //     properties: {},
    //     geometry: {
    //         type: 'Polygon',
    //         coordinates: [polygonFromQuad(quad)]
    //     }
    // };

    // Draw.add(feature2);
});