Skip to content

9 - Keyboard Navigation

This example uses keyboard events to interact with the map.

With a list of sorted sections, you can use LEFT and RIGHT arrows to navigate between them.

If you press ENTER, you'll zoom into the hovered section.

Once in the seats view, you can use the same key arrows to navigate between the section's seats.

You can select the hovered seat pressing ENTER.

If you press ESCAPE, or zoom-out, you'll go back to interact with the sections.

Map Viewer

Code

import { loadModule } from "@3ddv/dvm";

loadModule("map_viewer", {
    container: "viewer-container",
    styles_by_groups: true,
    instanced_nodes: true,
})
    .then((viewer) => {
        window.viewer = viewer;
        start(viewer);
    })
    .catch((err) => {
        console.error(err);
    });

function start(viewer) {
    // Just for the example purpose
    viewer.flags.scroll_with_mod_key = true;

    const venue_id = "nam-us-00096-chicagocubs";
    const map_id = "main";

    let sections = null;
    let section_index = -1;

    let seats = null;
    let seat_index = -1;

    viewer.subscribe("load_success", (_obj) => {
        // Set a random availability to have some unavailable nodes
        setSectionSeatAvailability(viewer);
        // Create a list of ordered and available sections. Unavailable sections are ignored
        sections = naturalSort(viewer.getNodesByState("section", "available").map(n => n.id));
        section_index = -1;
    });

    viewer.subscribe("reset", (_obj) => {
        resetAll();
    });

    viewer.subscribe("keydown", (obj) => {
        const event = obj.original_event;
        // Layer level 0 is section view, Layer level 1 is seat view. Depending on the current layer level, we'll do different actions
        const current_level = viewer.layers.getLayerLevel();
        console.log(event.key, event.keyCode);
        switch (event.keyCode) {
            case 37: // arrow left
                current_level === 0 ? prevSection() : prevSeat();
                break;
            case 39: // arrow right
                current_level === 0 ? nextSection() : nextSeat();
                break;
            // case 32: // space
            case 13: // enter
                current_level === 0 ? enterCurrentSection() : selectCurrentSeat();
                break;
            case 27: // Escape
                exitSection(true);
        }
    });

    function nextSection() {
        if (sections?.length) {
            section_index = (section_index + 1) % sections.length;
            hoverAndGoCurrentSection();
        }

    }

    function prevSection() {
        if (sections?.length) {
            --section_index;
            if (section_index < 0) section_index = sections.length - 1;
            hoverAndGoCurrentSection();
        }
    }

    function nextSeat() {
        if (seats?.length) {
            seat_index = (seat_index + 1) % seats.length;
            hoverAndGoCurrentSeat();
        }

    }

    function prevSeat() {
        if (seats?.length) {
            --seat_index;
            if (seat_index < 0) seat_index = seats.length - 1;
            hoverAndGoCurrentSeat();
        }
    }

    // Hover the current section. If it's not on screen, focus on it
    function hoverAndGoCurrentSection() {
        const section = viewer.getNodeById(sections?.[section_index]);
        if (section) {
            console.log("Hover section:", section.id);
            viewer.hover(section);
            if (!section.on_screen) viewer.focusOn(section, viewer.scaling_factor);
        }
    }

    // Hover the current seat. If it's not on screen, focus on it
    function hoverAndGoCurrentSeat() {
        const seat = viewer.getNodeById(seats?.[seat_index]);
        if (seat) {
            console.log("Hover seat:", seat.id);
            viewer.hover(seat);
            if (!seat.on_screen) viewer.focusOn(seat, viewer.scaling_factor);
        }
    }

    // Select the current seat. If it's not on screen, focus on it
    function selectCurrentSeat() {
        const seat = viewer.getNodeById(seats?.[seat_index]);
        if (seat) {
            console.log("Select seat:", seat.id);
            if (!seat.on_screen) viewer.goTo(seat);
            viewer.select(seat);
        }
    }

    // Zoom-in in with an animation the current section and make a list of its seats
    function enterCurrentSection() {
        const section = viewer.getNodeById(sections?.[section_index]);
        if (section) {
            console.log("Enter section:", section.id);
            viewer.goTo(section);
            seats = naturalSort(viewer.getNodesByState("seat", "available", section.id).map(n => n.id));
            seat_index = -1;
        }
    }

    function exitSection() {
        // Just zoom out
        viewer.goTo([0, 0], viewer.min_scaling_factor);
        resetSeats();
    }

    function resetSections() {
        sections = null;
        section_index = -1;
    }

    function resetSeats() {
        // Remove current seats data
        seats = null;
        seat_index = -1;
    }

    function resetAll() {
        resetSections();
        resetSeats();
    }


    // Load the map
    viewer.loadMap({ venue_id, map_id })
        .then(() => {
            // Successfully loaded
            console.log("LOADED!");
        })
        .catch((err) => {
            // Error while loading
            console.error(err);
        });

    function setSectionSeatAvailability(instance, prob = 0.6) {
        if (instance && instance.isLoaded()) {
            const section_availability = [];
            const seat_availability = [];
            viewer.getNodesByType("section").forEach((section) => {
                if (Math.random() < prob) {
                    section_availability.push(section.id);
                    section.children?.forEach((seat_id) => {
                        if (Math.random() < prob) {
                            seat_availability.push(seat_id);
                        }
                    });
                }
            });
            viewer.setAvailability("section", section_availability);
            viewer.setAvailability("seat", seat_availability);
        }
    }


    // https://stackoverflow.com/a/4373238
    function naturalSort(ary, fullNumbers) {
        var re = fullNumbers ? /[\d\.\-]+|\D+/g : /\d+|\D+/g;

        // Perform a Schwartzian transform, breaking each entry into pieces first
        for (let i = ary.length; i--;)
            ary[i] = [ary[i]].concat((ary[i] + "").match(re).map(function(s) {
                return isNaN(s) ? [s, false, s] : [s * 1, true, s];
            }));

        // Perform a cascading sort down the pieces
        ary.sort(function(a, b) {
            var al = a.length, bl = b.length, e = al > bl ? al : bl;
            for (let i = 1; i < e; ++i) {
                // Sort "a" before "a1"
                if (i >= al) return -1; else if (i >= bl) return 1;
                else if (a[i][0] !== b[i][0])
                    return (a[i][1] && b[i][1]) ?     // Are we comparing numbers?
                        (a[i][0] - b[i][0]) :         // Then diff them.
                        (a[i][2] < b[i][2]) ? -1 : 1; // Otherwise, lexicographic sort
            }
            return 0;
        });

        // Restore the original values into the array
        for (let i = ary.length; i--;) ary[i] = ary[i][0];
        return ary;
    }
}