Skip to content
Snippets Groups Projects
Commit 8750bfc8 authored by Ruben Backx's avatar Ruben Backx :coffee:
Browse files

Merge branch 'click-map-to-indicate-location' into 'development'

Add clicking maps to indicate location

Closes #196

See merge request !838
parents b1cf0bbc c7d0e877
Branches
Tags v0.6.1
2 merge requests!8412425.0.0 release,!838Add clicking maps to indicate location
Showing
with 267 additions and 33 deletions
......@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- Teachers can now create their own courses, if they are the associated teacher on study guide. [@cedricwilleken](https://gitlab.ewi.tudelft.nl/cedricwilleken)
- If a room has a map, students can now click the map to indicate their location. @rwbackx
### Changed
......
......@@ -24,6 +24,7 @@ import static nl.tudelft.queue.service.LabService.SessionType.REGULAR;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import jakarta.annotation.PostConstruct;
......@@ -170,7 +171,7 @@ public class DevDatabaseLoader implements InitializingBean {
&& e.getCourse().getName().equals("Object-Oriented Programming"))
.findFirst().get().getId()).block();
oopNow = eApi.getEditionById(editions.stream()
.filter(e -> "NOW".equals(e.getName())
.filter(e -> Pattern.compile("20\\d\\d/20\\d\\d Q1|NOW").matcher(e.getName()).matches()
&& e.getCourse().getName().equals("Object-Oriented Programming"))
.findFirst().get().getId()).block();
adsNow = eApi.getEditionById(editions.stream()
......
......@@ -30,6 +30,7 @@ import nl.tudelft.queue.dto.create.RequestCreateDTO;
import nl.tudelft.queue.dto.id.TimeSlotIdDTO;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.embeddables.AllowedRequest;
import nl.tudelft.queue.model.embeddables.Location;
import nl.tudelft.queue.model.enums.Language;
import nl.tudelft.queue.model.enums.OnlineMode;
import nl.tudelft.queue.model.enums.RequestType;
......@@ -59,6 +60,8 @@ public class LabRequestCreateDTO extends RequestCreateDTO<LabRequest, Lab> {
private Long room;
private Location location;
private OnlineMode onlineMode;
@Builder.Default
......
......@@ -22,6 +22,7 @@ import java.util.List;
import lombok.*;
import nl.tudelft.librador.dto.patch.Patch;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.embeddables.Location;
@Data
@Builder
......@@ -29,7 +30,9 @@ import nl.tudelft.queue.model.LabRequest;
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class RequestPatchDTO extends Patch<LabRequest> {
private Long room;
private Location location;
private String comment;
private String question;
......@@ -37,6 +40,7 @@ public class RequestPatchDTO extends Patch<LabRequest> {
@Override
protected void applyOneToOne() {
updateNonNull(room, data::setRoom);
data.setLocation(location);
updateNonNull(comment, data::setComment);
updateNonNull(question, data::setQuestion);
......
......@@ -38,6 +38,7 @@ import nl.tudelft.queue.csv.CsvAble;
import nl.tudelft.queue.model.QueueSession;
import nl.tudelft.queue.model.Request;
import nl.tudelft.queue.model.RequestEvent;
import nl.tudelft.queue.model.embeddables.Location;
@Data
@SuperBuilder
......@@ -54,6 +55,7 @@ public abstract class RequestViewDTO<R extends Request<?>> extends View<R>
private RequestEventInfoViewDTO eventInfo;
private RoomDetailsDTO room;
private Location location;
private PersonSummaryDTO requester;
private StudentGroupDetailsDTO studentGroup;
......
......@@ -35,6 +35,7 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import nl.tudelft.librador.dto.view.View;
import nl.tudelft.queue.model.embeddables.Location;
import nl.tudelft.queue.model.embeddables.RequestEventInfo;
@Data
......@@ -71,6 +72,16 @@ public abstract class Request<QS extends QueueSession<?>> {
*/
private Long room;
/**
* The location on the floorplan where the student is situated.
*/
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "x", column = @Column(name = "location_x")),
@AttributeOverride(name = "y", column = @Column(name = "location_y")),
})
private Location location;
/**
* The session this request takes part in.
*/
......
/*
* Queue - A Queueing system that can be used to handle labs in higher education
* Copyright (C) 2016-2024 Delft University of Technology
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package nl.tudelft.queue.model.embeddables;
import jakarta.persistence.Embeddable;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Embeddable
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Location {
@NotNull
private Double x;
@NotNull
private Double y;
}
......@@ -1487,3 +1487,20 @@ databaseChangeLog:
columnName: extra_info
tableName: queue_session
newDataType: text
- changeSet:
id: click-map-to-indicate-location
author: ruben (generated)
changes:
- addColumn:
columns:
- column:
name: location_x
type: DOUBLE PRECISION(53)
tableName: request
- addColumn:
columns:
- column:
name: location_y
type: DOUBLE PRECISION(53)
tableName: request
.indicator-holder {
position: relative;
overflow: hidden;
}
.indicator {
aspect-ratio: 1 / 1;
background-color: red;
border: 4px solid white;
border-radius: 100%;
outline: 4px solid red;
position: absolute;
transform: translate(-50%, -50%);
width: 1.25rem;
}
/*
* Queue - A Queueing system that can be used to handle labs in higher education
* Copyright (C) 2016-2024 Delft University of Technology
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
document.addEventListener("DOMContentLoaded", () => {
// On clicking interactable maps, update the indicator
document.querySelectorAll("[data-map][data-interactable]").forEach(imageHolder => {
imageHolder.addEventListener("click", event => {
const mapId = imageHolder.getAttribute("data-map");
if (event.target.tagName === "IMG") {
const bounds = event.target.getBoundingClientRect();
const location = {
x: event.offsetX / bounds.width,
y: event.offsetY / bounds.height,
};
updateIndicator(mapId, location);
}
});
});
// When a select with a 'data-update-map' changes, reset the indicator of corresponding maps
document.querySelectorAll("[data-update-map]").forEach(select => {
select.addEventListener("change", () => {
const mapId = select.getAttribute("data-update-map");
resetIndicator(mapId);
});
});
});
function updateIndicator(mapId, location) {
// Update all indicators
document.querySelectorAll(`[data-map="${mapId}"]`).forEach(imageHolder => {
let indicator = imageHolder.querySelector(".indicator");
if (!indicator) {
const indicatorHolder = imageHolder.querySelector(".indicator-holder");
indicator = document.createElement("div");
indicator.classList.add("indicator");
indicatorHolder.appendChild(indicator);
}
indicator.style.left = `${location.x * 100}%`;
indicator.style.top = `${location.y * 100}%`;
});
// Update all x coordinate fields
document.querySelectorAll(`[data-map-x="${mapId}"]`).forEach(locationX => {
locationX.name = "location.x";
locationX.value = location.x;
});
// Update all y coordinate fields
document.querySelectorAll(`[data-map-y="${mapId}"]`).forEach(locationY => {
locationY.name = "location.y";
locationY.value = location.y;
});
}
function resetIndicator(mapId) {
// Reset all indicators
document.querySelectorAll(`[data-map="${mapId}"]`).forEach(imageHolder => {
const indicator = imageHolder.querySelector(".indicator");
if (indicator) {
indicator.remove();
}
});
// Reset all x coordinate fields
document.querySelectorAll(`[data-map-x="${mapId}"]`).forEach(locationX => {
locationX.name = "";
});
// Reset all y coordinate fields
document.querySelectorAll(`[data-map-y="${mapId}"]`).forEach(locationY => {
locationY.name = "";
});
}
......@@ -15,26 +15,35 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
$(() => {
$("#input-room").change(() => {
// Get the room ID of the currently selected room.
const roomId = $("#input-room").find(":selected").attr("value");
updateRequestInfo(roomId);
document.addEventListener("DOMContentLoaded", async () => {
// Load all maps in elements with a 'data-room-id' attribute
document.querySelectorAll("[data-room-id]").forEach(imageHolder => {
loadMap(imageHolder);
});
// When a select with a 'data-update-map' attribute changes, update all the maps with a corresponding 'data-update-map' attribute
document.querySelectorAll("[data-update-map]").forEach(select => {
select.addEventListener("change", () => {
const roomId = select.value;
const mapId = select.getAttribute("data-update-map");
document.querySelectorAll(`[data-map="${mapId}"]`).forEach(imageHolder => {
imageHolder.setAttribute("data-room-id", roomId);
loadMap(imageHolder);
});
});
});
});
function updateRequestInfo(roomId) {
const imageHolder = document.getElementById("image-holder");
if (imageHolder == null) return;
async function loadMap(imageHolder) {
const image = imageHolder.querySelector("img");
$.get({
url: "/room/map/" + roomId,
success: function (response) {
const roomId = imageHolder.getAttribute("data-room-id");
try {
const response = await fetch(`/room/map/${roomId}`);
const fileName = (await response.json())["fileName"];
image.setAttribute("src", `/maps/${fileName}`);
imageHolder.removeAttribute("hidden");
image.setAttribute("src", `/maps/${JSON.parse(response).fileName}`);
},
error: function () {
} catch (error) {
// On error (most of the time 404, i.e. room has no image), hide the map
imageHolder.setAttribute("hidden", "");
},
});
}
}
......@@ -32,6 +32,10 @@
<!--@thymesVar id="message" type="java.lang.String"-->
<head>
<link rel="stylesheet" href="/css/room_map.css" />
</head>
<body>
<section layout:fragment="subcontent">
<h3 class="font-500 mb-3" th:text="|Enqueue for ${qSession.session.name}|"></h3>
......
......@@ -199,7 +199,7 @@
<label for="input-room">Room</label>
<div>
<select class="textfield" id="input-room" th:field="*{room}" data-select required>
<select class="textfield" id="input-room" th:field="*{room}" data-select data-update-map="enqueue" required>
<optgroup th:each="building : ${buildings}" th:label="${building.name}">
<option
th:each="room : ${rooms.?[#this.building.id == #root.building.id]}"
......@@ -263,9 +263,17 @@
<div class="flex vertical gap-1">
<label for="input-comment" id="labelComment">Help your TA find you!</label>
<div id="image-holder" class="mt-1 mb-3" hidden>
<div id="image-holder" data-map="enqueue" data-interactable class="mt-1 mb-3" hidden>
<div class="banner mb-3">
<span class="banner__icon fa-solid fa-circle-info"></span>
<p>You can click the map to indicate where you are located.</p>
</div>
<div class="indicator-holder">
<img alt="Room map" />
</div>
</div>
<input data-map-x="enqueue" type="hidden" />
<input data-map-y="enqueue" type="hidden" />
<textarea
maxlength="250"
......@@ -282,6 +290,7 @@
<script src="/webjars/momentjs/min/moment.min.js"></script>
<script src="/js/map_loader.js"></script>
<script src="/js/map_indicator.js"></script>
<script type="text/javascript" th:inline="javascript">
function checkEnrolled(val) {
......
......@@ -58,7 +58,7 @@
th:text="|${room.building.name} - ${room.name}|"></h1>
<div class="flex justify-center">
<div id="image-holder" hidden>
<div id="image-holder" data-map="presentation" th:data-room-id="${room.id}" hidden>
<img style="height: 24cqw" alt="Room map" />
</div>
</div>
......
......@@ -40,6 +40,7 @@
<title th:text="|Session ${qSession.id}|"></title>
<link rel="stylesheet" href="/css/request_colours.css" />
<link rel="stylesheet" href="/css/room_map.css" />
<script th:inline="javascript" type="text/javascript">
//<![CDATA[
......@@ -102,6 +103,7 @@
</div>
<script src="/js/map_loader.js"></script>
<script src="/js/map_indicator.js"></script>
<script type="text/javascript">
$(function () {
const roomId = $("select[name='room']").val();
......
......@@ -38,7 +38,7 @@
<div th:if="${current.room != null}" id="physical-location-info" class="flex vertical gap-3">
<div class="flex vertical gap-1 align-start">
<span th:text="|Current location: ${current.room?.building?.name} - ${current.room?.name}|"></span>
<select class="textfield" th:field="*{room}" data-style="variant">
<select id="input-room" class="textfield" th:field="*{room}" data-style="variant" data-update-map="update-request">
<option disabled value="">Choose your new room</option>
<option
th:each="room : ${rooms}"
......@@ -50,6 +50,27 @@
<div class="flex vertical gap-1" id="comment">
<label for="inputComment">Where are you located?</label>
<div
class="mt-1 mb-3"
id="image-holder"
data-map="update-request"
data-interactable
th:data-room-id="${current.room.id}"
hidden>
<div class="indicator-holder">
<img alt="Room map" />
<th:block th:unless="${current.location == null}">
<div
class="indicator"
th:style="|left: ${current.location.x * 100}%; top: ${current.location.y * 100}%;|"></div>
</th:block>
</div>
</div>
<input data-map-x="update-request" type="hidden" />
<input data-map-y="update-request" type="hidden" />
<textarea
type="text"
class="textfield"
......@@ -80,8 +101,6 @@
<button type="submit" class="button" name="updateRequestInfo">Save</button>
</div>
</form>
<div id="image-holder"></div>
</div>
</div>
</th:block>
......
......@@ -28,6 +28,8 @@
<head>
<title th:text="${'Request #' + request.id}"></title>
<link rel="stylesheet" href="/css/room_map.css" />
<style>
.btn-group.feedback > .btn.focus {
transition: 0.5s;
......@@ -110,6 +112,7 @@
</div>
<script src="/js/map_loader.js"></script>
<script src="/js/map_indicator.js"></script>
<script type="text/javascript" th:inline="javascript">
const roomId = /*[[${request.room != null} ? ${request.room.id}]]*/ 0;
updateRequestInfo(roomId);
......
......@@ -159,14 +159,21 @@
</div>
</div>
<div id="image-holder" class="mt-5" hidden>
<th:block th:unless="${request.room == null}">
<div id="image-holder" class="mt-5" th:data-map="|request-${request.id}|" th:data-room-id="${request.room.id}" hidden>
<div class="surface p-0">
<h3 class="surface__header">Room map</h3>
<div class="surface__content">
<div class="indicator-holder">
<img alt="Room map" />
<th:block th:unless="${request.location == null}">
<div class="indicator" th:style="|left: ${request.location.x * 100}%; top: ${request.location.y * 100}%;|"></div>
</th:block>
</div>
</div>
</div>
</div>
</th:block>
<div class="mt-5" th:if="${@permissionService.canViewRequestAssistantReason(request.id)}">
<h3 class="font-500 mb-3">Previous</h3>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment