diff --git a/README.md b/README.md index d1870c7..d9a7331 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,74 @@ # EventMapper -TODO: Readme \ No newline at end of file +EventMapper, an easily-deployable, nearly-static digital guide application made in JavaScript. + +## Specifications + +### Data Folder Structure +``` +/events + [FLOOR].json +/rooms + [FLOOR].json +floors.json +``` + +### Floor +Used to represent a floor in a building. It is a parent to both its [Rooms](#room) and [Events](#events). +This object gets represented in the app as a map poly, and in a set of buttons among other floors. +```json +{ + "id": "[FLOOR ID]", + "poly": [ + [x, y], // An array of vertex positions. + ... + ], + "lang": { + "[IETF LANGUAGE TAG]": { + "name": "[SHORT SINGLE-LINE STRING]", + "description": "[SINGLE-LINE STRING]" + } + } +} +``` +This will be stored in an array in `floors.json` to be accessed by clients. + +### Room +Used to represent a space in which events can happen. It is a child to a [Floor](#floor), and a parent to [Events](#event). +This object gets represented in the app as a map poly. +```json +{ + "id": "[FLOOR]_[ROOM ID]", + "poly": [ + [x, y], // An array of vertex positions. + ... + ], + "lang": { + "[IETF LANGUAGE TAG]": { + "name": "[SINGLE-LINE STRING]", + "description": "[SINGLE-LINE STRING]" + } + } +} +``` +This will be stored in an array in `rooms/[FLOOR].json` to be accessed by clients. + +### Event +Used to represent a point in time when an activity or festivity happens. It is a child to both a [Floor](#floor) and a [Room](#room), reflected in its ID. +```json +{ + "id": "[FLOOR]_[ROOM]_[EVENT ID]", + "when": { + "start": "[ISO 8601 DATE]", + "end": "[ISO 8601 DATE]" + }, + "lang": { + "[IETF LANGUAGE TAG]": { + "name": "[SINGLE-LINE STRING]", + "description": "[MULTI-LINE STRING]", + "url": "[URL]" + } + } +} +``` +This will be stored in an array in `events/[FLOOR].json` to be accessed by clients. \ No newline at end of file diff --git a/assets/scripts/KaplayMap/gameobj.js b/assets/scripts/KaplayMap/gameobj.js index a31316c..5872c24 100644 --- a/assets/scripts/KaplayMap/gameobj.js +++ b/assets/scripts/KaplayMap/gameobj.js @@ -1,8 +1,4 @@ export class GameObjManager { - opts = { - floorMinZoom: 1, - roomMinZoom: 2, - }; map; mainmanager; @@ -10,12 +6,8 @@ export class GameObjManager { floorObject; roomObjects = new Map([]); - constructor(map, opts) { + constructor(map) { this.map = map; - this.opts = { - ...this.opts, - opts, - }; } generateFloor() { @@ -58,13 +50,32 @@ export class GameObjManager { this.map.kp.add(this.floorObject); } + zoomToBounds(boundingBox) { + // Get screen size + const width = this.map.kp.width(); + const height = this.map.kp.height(); + + // Get bbox size + const bBoxWidth = boundingBox.width * 1.5; + const bBoxHeight = boundingBox.height * 1.5; + + // Compare bounds + const scaledWidth = width / bBoxWidth; + const scaledHeight = height / bBoxHeight; + + // Whichever one is biggest + const scale = Math.min(scaledWidth, scaledHeight); + + this.map.zoomToAbs( + Math.log2(scale), // log scale + boundingBox.center() + ); + } + zoomToFloor() { const objectBounds = this.floorObject.renderArea().bbox(); - this.map.zoomToAbs( - this.map.opts.minZoomLevel + this.opts.floorMinZoom, - objectBounds.center() - ); + this.zoomToBounds(objectBounds); } zoomToRoom(id) { @@ -73,10 +84,7 @@ export class GameObjManager { const objectBounds = selectedObject.renderArea().bbox(); - this.map.zoomToAbs( - this.map.opts.minZoomLevel + this.opts.roomMinZoom, - objectBounds.center() - ); + this.zoomToBounds(objectBounds); } generateRooms() { diff --git a/assets/scripts/KaplayMap/localization.js b/assets/scripts/KaplayMap/localization.js index dbc7884..d951ef5 100644 --- a/assets/scripts/KaplayMap/localization.js +++ b/assets/scripts/KaplayMap/localization.js @@ -15,12 +15,10 @@ export default new Map([ events_in: (name) => `Events (${name})`, event_in: (location) => `In ${location}`, view_events_in: (name) => `Return to events in ${name}`, - minimize: "Hide events panel", - maximize: "Show events panel", + minimize: "Toggle event panel docking", event_inspector_header: (name) => `Event ${name}`, event_inspector_back: "Return to events panel", - event_inspector_minimize: "Hide event inspector", - event_inspector_maximize: "Show event inspector", + event_inspector_minimize: "Toggle event inspector docking", }, ], ]); diff --git a/assets/scripts/KaplayMap/map.js b/assets/scripts/KaplayMap/map.js index f690cdc..386293e 100644 --- a/assets/scripts/KaplayMap/map.js +++ b/assets/scripts/KaplayMap/map.js @@ -2,7 +2,7 @@ export class KaplayMap { kp; opts = { minZoomLevel: 1, - maxZoomLevel: 5, + maxZoomLevel: 6, dblClickDuration: 0.2, // s dblClickForgiveness: 100, // px }; diff --git a/assets/scripts/KaplayMap/mapper.js b/assets/scripts/KaplayMap/mapper.js index 4bd9bb0..bf1aa18 100644 --- a/assets/scripts/KaplayMap/mapper.js +++ b/assets/scripts/KaplayMap/mapper.js @@ -29,7 +29,7 @@ export class EventMapperManager { this.uimanager = new UIManager(mapUi); this.uimanager.mainmanager = this; - this.gameobjmanager = new GameObjManager(this.map, {}); + this.gameobjmanager = new GameObjManager(this.map); this.gameobjmanager.mainmanager = this; } @@ -60,17 +60,24 @@ export class EventMapperManager { let hash = location.hash; if (hash.includes("#")) hash = hash.replace("#", ""); + else return; const floorFocus = this.convertIdFocus(hash, "floor"); const roomFocus = this.convertIdFocus(hash, "room"); const eventFocus = this.convertIdFocus(hash, "event"); + this.uimanager.setLoading(true); if (floorFocus != this.convertFocus(prevID, "floor")) await this.selectFloor(floorFocus); + this.uimanager.setLoading(true); + this.uimanager.setLoading(false); if (roomFocus != this.convertFocus(prevID, "room")) await this.selectRoom(roomFocus); + this.uimanager.setLoading(true); + this.uimanager.setLoading(false); if (eventFocus != this.convertFocus(prevID, "event")) await this.selectEvent(eventFocus); + this.uimanager.setLoading(false); } async selectFloor(id) { diff --git a/assets/scripts/KaplayMap/ui.js b/assets/scripts/KaplayMap/ui.js index 0351382..26151d4 100644 --- a/assets/scripts/KaplayMap/ui.js +++ b/assets/scripts/KaplayMap/ui.js @@ -49,7 +49,9 @@ export class UIManager { } setEventsMinimized(state) { - if (state) { + if (state == null) { + this.uiElements.eventsContainer.classList.toggle("minimized"); + } else if (state) { this.uiElements.eventsContainer.classList.add("minimized"); } else { this.uiElements.eventsContainer.classList.remove("minimized"); @@ -57,7 +59,10 @@ export class UIManager { } setEventsInspector(state) { - this.setEventsMinimized(false); + if ( + state != this.uiElements.eventsContainer.classList.contains("inspector") + ) + this.setEventsMinimized(false); if (state) { this.uiElements.eventsContainer.classList.add("inspector"); } else { @@ -81,9 +86,6 @@ export class UIManager { // Floors this.uiElements.floors = this.ui.querySelector("#floors"); - this.uiElements.floorsHeading = this.ui.querySelector( - "#floors .widget-heading" - ); this.uiElements.floorButtons = this.uiElements.floors.querySelector("#floor-buttons"); @@ -102,10 +104,6 @@ export class UIManager { this.uiElements.events.querySelector("#events-header #room-description"); this.uiElements.eventList = this.uiElements.events.querySelector("#event-list"); - this.uiElements.eventsMinimized = - this.uiElements.eventsContainer.querySelector("#events-minimized"); - this.uiElements.eventsMinimizedBackButton = - this.uiElements.eventsMinimized.querySelector("#back"); this.uiElements.eventsInspector = this.uiElements.eventsContainer.querySelector("#events-inspector"); @@ -117,29 +115,16 @@ export class UIManager { this.uiElements.eventsInspectorHeader.querySelector("#event-length"); this.uiElements.eventsInspectorRoomTime = this.uiElements.eventsInspector.querySelector("#room-time"); - this.uiElements.eventsInspectorMinimized = - this.uiElements.eventsContainer.querySelector( - "#events-inspector-minimized" - ); this.uiElements.eventsInspectorBackButton = this.uiElements.eventsInspector.querySelector("#back"); this.uiElements.eventsInspectorBody = this.uiElements.eventsInspector.querySelector("#event-body"); - this.uiElements.eventsInspectorMinimizedBackButton = - this.uiElements.eventsInspectorMinimized.querySelector("#back"); - this.uiElements.minimizeButton = this.uiElements.events.querySelector( - "#footer #footer-buttons #minimize" - ); - this.uiElements.maximizeButton = - this.uiElements.eventsMinimized.querySelector("#maximize"); + this.uiElements.minimizeButton = + this.uiElements.events.querySelector("#minimize"); this.uiElements.eventsInspectorMinimizeButton = - this.uiElements.eventsInspector.querySelector( - "#footer #footer-buttons #minimize" - ); - this.uiElements.eventsInspectorMaximizeButton = - this.uiElements.eventsInspectorMinimized.querySelector("#maximize"); + this.uiElements.eventsInspector.querySelector("#minimize"); // Loading this.uiElements.loading = this.ui.querySelector("#loading"); @@ -166,21 +151,11 @@ export class UIManager { __initInspector() { this.uiElements.eventsInspectorMinimizeButton.addEventListener( "click", - () => this.setEventsMinimized(true) - ); - this.uiElements.eventsInspectorMaximizeButton.addEventListener( - "click", - () => this.setEventsMinimized(false) + () => this.setEventsMinimized() ); this.uiElements.eventsInspectorBackButton.addEventListener("click", () => { window.location.hash = this.mainmanager.getCurrentFocus("room"); }); - this.uiElements.eventsInspectorMinimizedBackButton.addEventListener( - "click", - () => { - window.location.hash = this.mainmanager.getCurrentFocus("room"); - } - ); } updateInspector() { @@ -191,12 +166,8 @@ export class UIManager { this.uiElements.eventsInspectorBackButton.title = currentLocalization.event_inspector_back; - this.uiElements.eventsInspectorMinimizedBackButton.title = - currentLocalization.event_inspector_back; this.uiElements.eventsInspectorMinimizeButton.title = currentLocalization.event_inspector_minimize; - this.uiElements.eventsInspectorMaximizeButton.title = - currentLocalization.event_inspector_maximize; const currentDate = new Date(); const eventDateStart = new Date(currentEvent.when.start); @@ -283,16 +254,10 @@ export class UIManager { this.uiElements.eventsBackButton.addEventListener("click", () => { window.location.hash = this.mainmanager.getCurrentFocus("floor"); }); - this.uiElements.eventsMinimizedBackButton.addEventListener("click", () => { - window.location.hash = this.mainmanager.getCurrentFocus("floor"); - }); - this.uiElements.minimizeButton.addEventListener("click", () => { - this.uiElements.eventsContainer.classList.add("minimized"); - }); - this.uiElements.maximizeButton.addEventListener("click", () => { - this.uiElements.eventsContainer.classList.remove("minimized"); - }); + this.uiElements.minimizeButton.addEventListener("click", () => + this.setEventsMinimized() + ); } __updateEventsSoft() { @@ -314,7 +279,6 @@ export class UIManager { const focusedRoom = this.mainmanager.getCurrentFocusObject("room"); this.uiElements.minimizeButton.title = currentLocalization.minimize; - this.uiElements.maximizeButton.title = currentLocalization.maximize; // Figure out header if (this.mainmanager.getIsFocused("room")) { @@ -329,9 +293,6 @@ export class UIManager { this.uiElements.eventsBackButton.disabled = false; this.uiElements.eventsBackButton.title = currentLocalization.view_events_in(focusedFloor.name); - this.uiElements.eventsMinimizedBackButton.disabled = false; - this.uiElements.eventsMinimizedBackButton.title = - currentLocalization.view_events_in(focusedFloor.name); } else { if (currentEvents.size < 1) { this.uiElements.eventsRoomName.textContent = focusedFloor.name; @@ -343,8 +304,6 @@ export class UIManager { focusedFloor.description; this.uiElements.eventsBackButton.disabled = true; this.uiElements.eventsBackButton.title = ""; - this.uiElements.eventsMinimizedBackButton.disabled = true; - this.uiElements.eventsMinimizedBackButton.title = ""; } // Remove all children @@ -455,8 +414,6 @@ export class UIManager { const mainFocus = this.mainmanager.getCurrentFocus("floor"); // Remove all children this.uiElements.floorButtons.replaceChildren(); - this.uiElements.floorsHeading.textContent = - currentLocalization.floors_header; // Put them back currentFloors.forEach(({ name }, id) => { @@ -486,9 +443,6 @@ export class UIManager { const currentFloors = this.mainmanager.getAllFocusObject("floor"); const mainFocus = this.mainmanager.getCurrentFocus("floor"); - this.uiElements.floorsHeading.textContent = - currentLocalization.floors_header; - currentFloors.forEach((_, id) => { const child = this.uiElements.floorButtons.querySelector("#floor-" + id); if (id === mainFocus) child.classList.add("selected"); diff --git a/assets/styles/style.css b/assets/styles/style.css index 8891047..e7a56ad 100644 --- a/assets/styles/style.css +++ b/assets/styles/style.css @@ -15,7 +15,7 @@ h1.widget-heading { margin: 0; margin-bottom: 4px; font-size: 14px; - letter-spacing: 0.25em; + /* letter-spacing: 0.25em; */ text-transform: uppercase; /* border-bottom: 1px solid currentColor; padding-bottom: 2px; */ @@ -136,15 +136,13 @@ a { } #map-ui #floors #floor-buttons .floor-button { - text-align: left; - width: 200px; - padding: 12px; - height: unset; + display: inline-block; + width: 36px; + height: 36px; + padding: 4px; } #map-ui #floors #floor-buttons .floor-button.selected { - padding-left: 9px; border: 1px solid #fff; - border-left: 4px solid #fff; } #map-ui #events-container { @@ -156,51 +154,57 @@ a { } #map-ui #events-container #events, -#map-ui #events-container #events-minimized, -#map-ui #events-container #events-inspector, -#map-ui #events-container #events-inspector-minimized { +#map-ui #events-container #events-inspector { pointer-events: all; box-sizing: border-box; - display: inline-block; + display: inline-flex; position: absolute; + gap: 8px; padding: 8px; + left: 16px; bottom: 16px; border-radius: 4px; - left: -100%; + translate: 0 calc(100% + 32px); background-color: #202020; color: #fff; box-shadow: 0 0 8px 0 #0008; - transition: left 0.5s; -} - -#map-ui #events-container #events, -#map-ui #events-container #events-inspector { + transition: translate 0.25s, bottom 0.25s, width 0.25s, border-radius 0.25s; + max-height: calc(50vh - 16px); + flex-direction: column; width: 400px; max-width: calc(100% - 32px); + z-index: 9; } -#map-ui #events-container:not(.empty):not(.inspector):not(.minimized) #events { - left: 16px; +#map-ui #events-container:not(.inspector) #events, +#map-ui #events-container.inspector #events-inspector { + translate: 0; } -#map-ui #events-container:not(.empty):not(.inspector).minimized #events-minimized { - left: 16px; +#map-ui #events-container:not(.inspector) #events, +#map-ui #events-container.inspector #events-inspector { + translate: 0; } -#map-ui #events-container:not(.empty):not(.minimized).inspector #events-inspector { - left: 16px; +#map-ui #events-container:not(.inspector).minimized #events, +#map-ui #events-container.inspector.minimized #events-inspector { + bottom: 0; + translate: 0 calc(100% - 52px); } -#map-ui #events-container:not(.empty).minimized.inspector #events-inspector-minimized { - left: 16px; +#map-ui #events-container.empty #events, +#map-ui #events-container.empty #events-inspector { + translate: 0 calc(100% + 32px); } -/* #map-ui #floors.empty ~ #events-container #events, -#map-ui #floors.empty ~ #events-container #events-inspector, -#map-ui #floors.empty ~ #events-container #events-minimized, -#map-ui #floors.empty ~ #events-container #events-inspector-minimized { - left: -100% !important; -} */ +#map-ui #events-container.minimized #events #room-description, +#map-ui #events-container.minimized #events-inspector #event-length { + display: none; +} + +#map-ui #events-container.minimized #room-name { + margin: 0; +} #map-ui #events-container #events #footer #footer-buttons, #map-ui #events-container #events-inspector #footer #footer-buttons { @@ -214,28 +218,15 @@ a { margin-bottom: 0; } -#map-ui #events-container #events #events-header, -#map-ui #events-container #events-inspector #events-header { +#map-ui #events-container #events-header { display: grid; - grid-template-columns: 36px auto; + align-items: start; + grid-template-columns: 36px auto 36px; gap: 8px; - margin-bottom: 8px; - /* align-items: center; */ } -#map-ui #events-container #events #event-list { - /* min-height: 100px; */ - max-height: calc(50% - 16px); - margin: 8px 0; - padding: 8px; - background-color: #0002; - display: flex; - flex-direction: column; - gap: 4px; - /* transition: height 0.25s, - min-height 0.25s, - padding 0.25s, - margin 0.25s; */ +#map-ui #events-container.minimized #events-header { + align-items: center; } #map-ui #events-container #events.empty #event-list { @@ -248,19 +239,23 @@ a { margin: 0; } +#map-ui #events-container #events #event-list, #map-ui #events-container #events-inspector #event-body { - /* min-height: 100px; */ - max-height: calc(50% - 16px); - margin: 8px 0; + width: 100%; + overflow-y: auto; + overflow-x: hidden; + box-sizing: border-box; padding: 8px; background-color: #0002; + display: flex; + flex-direction: column; + gap: 4px; + display: inline-block; } #map-ui #events-container #events #events-header button, #map-ui #events-container #footer #footer-buttons button, -#map-ui #events-container #events-inspector #events-header button:not(.link), -#map-ui #events-container #events-minimized button, -#map-ui #events-container #events-inspector-minimized button { +#map-ui #events-container #events-inspector #events-header button:not(.link) { width: 36px; } @@ -292,4 +287,14 @@ a { -webkit-mask-image: radial-gradient(circle, #fff0 0%, #fff0 33%, #ffff 33%, #ffff 66%, #fff0 66%, #fff0 100%); mask-image: radial-gradient(circle, #fff0 0%, #fff0 33%, #ffff 33%, #ffff 66%, #fff0 66%, #fff0 100%); animation: load 0.5s linear infinite; +} + +@media (max-width: 640px) { + #map-ui #events-container #events, + #map-ui #events-container #events-inspector { + bottom: 0; + border-radius: 4px 4px 0 0; + width: 100%; + max-height: 33vh; + } } \ No newline at end of file diff --git a/pages/index.html b/pages/index.html index 57b8f91..1767a32 100644 --- a/pages/index.html +++ b/pages/index.html @@ -19,7 +19,6 @@
-

@@ -32,19 +31,11 @@

+
- - -
- -
@@ -53,19 +44,11 @@

+

- - -
- -
diff --git a/scripts/server.js b/scripts/server.js index 71fda2c..e9a9787 100644 --- a/scripts/server.js +++ b/scripts/server.js @@ -19,21 +19,14 @@ app.get('/data/:lang/events/:floor', async (req, res) => { else return res.status(400).send("Bad Request"); - // Get localization - let l10nReq = await fetch(new URL(`localization/${req.params.lang}.json`, config.data_url)); - let l10n; - if (l10nReq.ok) - l10n = await l10nReq.json(); - else - return res.status(404).send("Localization not found!"); - // Merge events and localization + const lang = req.params.lang; const merged = events.map(event => { - const localizationKey = l10n.__events[event.id]; - return { - ...event, - ...localizationKey - }; + event.name = event.lang[lang]?.name ?? ""; + event.description = event.lang[lang]?.description ?? ""; + event.url = event.lang[lang]?.url ?? ""; + delete event.lang; + return event; }); return res.send(merged); @@ -48,21 +41,13 @@ app.get('/data/:lang/floors', async (req, res) => { else return res.status(400).send("Bad Request"); - // Get localization - let l10nReq = await fetch(new URL(`localization/${req.params.lang}.json`, config.data_url)); - let l10n; - if (l10nReq.ok) - l10n = await l10nReq.json(); - else - return res.status(404).send("Localization not found!"); - // Merge layers and localization + const lang = req.params.lang; const merged = layers.map(layer => { - const localizationKey = l10n.__layers[layer.id]; - return { - ...layer, - ...localizationKey - }; + layer.name = layer.lang[lang]?.name ?? ""; + layer.description = layer.lang[lang]?.description ?? ""; + delete layer.lang; + return layer; }); return res.send(merged); @@ -77,21 +62,13 @@ app.get('/data/:lang/rooms/:floor', async (req, res) => { else return res.status(400).send("Bad Request"); - // Get localization - let l10nReq = await fetch(new URL(`localization/${req.params.lang}.json`, config.data_url)); - let l10n; - if (l10nReq.ok) - l10n = await l10nReq.json(); - else - return res.status(404).send("Localization not found!"); - // Merge rooms and localization + const lang = req.params.lang; const merged = rooms.map(room => { - const localizationKey = l10n.__rooms[room.id]; - return { - ...room, - ...localizationKey - }; + room.name = room.lang[lang]?.name ?? ""; + room.description = room.lang[lang]?.description ?? ""; + delete room.lang; + return room; }); return res.send(merged); @@ -101,4 +78,6 @@ app.listen(config.port, () => { const listeningURL = new URL("http://localhost/"); listeningURL.port = config.port; console.log(`Example app listening on ${listeningURL.toString()}`) -}) \ No newline at end of file +}) + +export default app; \ No newline at end of file