First Iteration of Map Question Type
This commit is contained in:
@@ -861,12 +861,12 @@ def new_question(request):
|
|||||||
messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.')
|
messages.error(request, 'Quiz nicht gefunden oder keine Berechtigung.')
|
||||||
return redirect('library:overview_quiz')
|
return redirect('library:overview_quiz')
|
||||||
|
|
||||||
if question_type not in ['multiple_choice', 'true_false','input','order','guess']:
|
if question_type not in ['multiple_choice', 'true_false','input','order','guess','map']:
|
||||||
base_url = reverse('library:new_question')
|
base_url = reverse('library:new_question')
|
||||||
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
|
return redirect(f'{base_url}?type=multiple_choice&quiz_id={quiz_id}')
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
question_text = request.POST.get('question', '')
|
question_text = request.POST.get('question', 'Kein Fragentext angegeben')
|
||||||
|
|
||||||
# Handle different question types
|
# Handle different question types
|
||||||
if question_type == 'true_false':
|
if question_type == 'true_false':
|
||||||
@@ -946,20 +946,27 @@ def new_question(request):
|
|||||||
max_value = request.POST.get('max')
|
max_value = request.POST.get('max')
|
||||||
tolerance = request.POST.get('tolerance')
|
tolerance = request.POST.get('tolerance')
|
||||||
json_data = {
|
json_data = {
|
||||||
'type': question_type,
|
'type': question_type,
|
||||||
'question': question_text,
|
'question': question_text,
|
||||||
'correct_answer': correct_answer,
|
'correct_answer': correct_answer,
|
||||||
'min': min_value,
|
'min': min_value,
|
||||||
'max': max_value,
|
'max': max_value,
|
||||||
'tolerance': tolerance,
|
'tolerance': tolerance,
|
||||||
'options': [
|
'options': [
|
||||||
{'value': correct_answer, 'is_correct': True}
|
{'value': correct_answer, 'is_correct': True}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
elif question_type == 'map':
|
||||||
|
target_location = request.POST.get('location')
|
||||||
|
tolerance_radius = request.POST.get('tolerance_radius')
|
||||||
|
|
||||||
|
json_data = {
|
||||||
|
'type': question_type,
|
||||||
|
'question': question_text,
|
||||||
|
'target_location': target_location,
|
||||||
|
'tolerance_radius': tolerance_radius
|
||||||
|
}
|
||||||
question = QivipQuestion()
|
question = QivipQuestion()
|
||||||
question.data = json.dumps(json_data)
|
question.data = json.dumps(json_data)
|
||||||
question.quiz_id = quiz
|
question.quiz_id = quiz
|
||||||
@@ -1030,18 +1037,26 @@ def new_question(request):
|
|||||||
max_value = request.POST.get('max')
|
max_value = request.POST.get('max')
|
||||||
tolerance = request.POST.get('tolerance')
|
tolerance = request.POST.get('tolerance')
|
||||||
json_data = {
|
json_data = {
|
||||||
'type': question_type,
|
'type': question_type,
|
||||||
'question': '',
|
'question': '',
|
||||||
'correct_answer': correct_answer,
|
'correct_answer': correct_answer,
|
||||||
'min': min_value,
|
'min': min_value,
|
||||||
'max': max_value,
|
'max': max_value,
|
||||||
'tolerance': tolerance,
|
'tolerance': tolerance,
|
||||||
'options': [
|
'options': [
|
||||||
{'value': correct_answer, 'is_correct': True}
|
{'value': correct_answer, 'is_correct': True}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
standard_time_per_question = 30
|
standard_time_per_question = 30
|
||||||
|
|
||||||
|
elif question_type == 'map':
|
||||||
|
question_data = {
|
||||||
|
'type': question_type,
|
||||||
|
'question': '',
|
||||||
|
'target_location': "",
|
||||||
|
'tolerance_radius': 90000
|
||||||
|
}
|
||||||
|
standard_time_per_question = 30
|
||||||
|
|
||||||
template_name = f'library/question/question_{question_type}.html'
|
template_name = f'library/question/question_{question_type}.html'
|
||||||
context = {
|
context = {
|
||||||
@@ -1083,6 +1098,13 @@ def edit_question(request, pk):
|
|||||||
{'value': 'Falsch', 'is_correct': not correct_answer}
|
{'value': 'Falsch', 'is_correct': not correct_answer}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
elif question_type == 'map':
|
||||||
|
question_data = {
|
||||||
|
'type': question_type,
|
||||||
|
'question': question_data.get('question', ''),
|
||||||
|
'target_location': question_data.get('target_location', ''),
|
||||||
|
'tolerance_radius': question_data.get('tolerance_radius', 90000)
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
# For multiple choice questions
|
# For multiple choice questions
|
||||||
question_data.setdefault('type', question_type)
|
question_data.setdefault('type', question_type)
|
||||||
@@ -1200,17 +1222,27 @@ def edit_question(request, pk):
|
|||||||
max_value = request.POST.get('max')
|
max_value = request.POST.get('max')
|
||||||
tolerance = request.POST.get('tolerance')
|
tolerance = request.POST.get('tolerance')
|
||||||
json_data = {
|
json_data = {
|
||||||
'type': question_type,
|
'type': question_type,
|
||||||
'question': question_text,
|
'question': question_text,
|
||||||
'correct_answer': correct_answer,
|
'correct_answer': correct_answer,
|
||||||
'min': min_value,
|
'min': min_value,
|
||||||
'max': max_value,
|
'max': max_value,
|
||||||
'tolerance': tolerance,
|
'tolerance': tolerance,
|
||||||
'options': [
|
'options': [
|
||||||
{'value': correct_answer, 'is_correct': True}
|
{'value': correct_answer, 'is_correct': True}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
elif question_type == 'map':
|
||||||
|
target_location = request.POST.get('location')
|
||||||
|
tolerance_radius = request.POST.get('tolerance_radius')
|
||||||
|
|
||||||
|
json_data = {
|
||||||
|
'type': question_type,
|
||||||
|
'question': question_text,
|
||||||
|
'target_location': target_location,
|
||||||
|
'tolerance_radius': tolerance_radius
|
||||||
|
}
|
||||||
question.data = json.dumps(json_data)
|
question.data = json.dumps(json_data)
|
||||||
question.time_per_question = request.POST.get('time_per_question', '30')
|
question.time_per_question = request.POST.get('time_per_question', '30')
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from django.utils import timezone
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from play.models import QuizGame, QuizGameParticipant, QuizAnswer
|
from play.models import QuizGame, QuizGameParticipant, QuizAnswer
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from geopy.distance import geodesic
|
||||||
|
|
||||||
class GameConsumer(AsyncWebsocketConsumer):
|
class GameConsumer(AsyncWebsocketConsumer):
|
||||||
|
|
||||||
@@ -31,6 +32,13 @@ class GameConsumer(AsyncWebsocketConsumer):
|
|||||||
# Check if game should be deleted
|
# Check if game should be deleted
|
||||||
await self.cleanup_game()
|
await self.cleanup_game()
|
||||||
|
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def get_question_data(self):
|
||||||
|
quiz_game = QuizGame.objects.get(join_code=self.join_code)
|
||||||
|
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
|
||||||
|
return json.loads(current_question.data)
|
||||||
|
|
||||||
async def receive(self, text_data):
|
async def receive(self, text_data):
|
||||||
text_data_json = json.loads(text_data)
|
text_data_json = json.loads(text_data)
|
||||||
message_type = text_data_json['type']
|
message_type = text_data_json['type']
|
||||||
@@ -84,14 +92,16 @@ class GameConsumer(AsyncWebsocketConsumer):
|
|||||||
|
|
||||||
elif message_type == 'submit_answer':
|
elif message_type == 'submit_answer':
|
||||||
participant_id = text_data_json['participant_id']
|
participant_id = text_data_json['participant_id']
|
||||||
try:
|
answer = text_data_json['answer']
|
||||||
answer = text_data_json['answer']
|
|
||||||
except:
|
|
||||||
answer=-1
|
|
||||||
time_remaining = text_data_json.get('time_remaining', 0)
|
time_remaining = text_data_json.get('time_remaining', 0)
|
||||||
|
|
||||||
# Save answer and update participant score
|
|
||||||
await self.save_answer(participant_id, answer, time_remaining)
|
question_data = await self.get_question_data()
|
||||||
|
if question_data['type'] == "map":
|
||||||
|
await self.save_answer_map(participant_id, answer, time_remaining)
|
||||||
|
else:
|
||||||
|
await self.save_answer(participant_id, answer, time_remaining)
|
||||||
|
|
||||||
|
|
||||||
# Notify host about the new answer
|
# Notify host about the new answer
|
||||||
await self.channel_layer.group_send(
|
await self.channel_layer.group_send(
|
||||||
@@ -182,6 +192,97 @@ class GameConsumer(AsyncWebsocketConsumer):
|
|||||||
except QuizGame.DoesNotExist:
|
except QuizGame.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@database_sync_to_async
|
||||||
|
def save_answer_map(self, participant_id, answer_location, time_remaining):
|
||||||
|
answer_index = -1
|
||||||
|
try:
|
||||||
|
# Get all necessary data first
|
||||||
|
participant = QuizGameParticipant.objects.get(
|
||||||
|
quiz_game__join_code=self.join_code,
|
||||||
|
participant_id=participant_id
|
||||||
|
)
|
||||||
|
quiz_game = participant.quiz_game
|
||||||
|
current_question = quiz_game.quiz_id.questions.all()[quiz_game.current_question_index]
|
||||||
|
question_data = json.loads(current_question.data)
|
||||||
|
target_location = question_data['target_location']
|
||||||
|
tolerance_radius = question_data['tolerance_radius']
|
||||||
|
print(f"target_location: {target_location}")
|
||||||
|
print(f"answer_location: {answer_location}")
|
||||||
|
print(f"tolerance_radius: {tolerance_radius}")
|
||||||
|
|
||||||
|
# Configuration - Make these easily adjustable
|
||||||
|
MAX_DISTANCE_KM = 1000 # Maximum distance for scoring (in km)
|
||||||
|
DIRECT_HIT_BONUS = 200 # Extra points for direct hit within tolerance
|
||||||
|
|
||||||
|
# Calculate base score
|
||||||
|
distance_km = geodesic(answer_location, target_location).kilometers
|
||||||
|
tolerance_km = int(tolerance_radius) / 1000 # Convert meters to kilometers
|
||||||
|
|
||||||
|
# Calculate base score with non-linear decrease
|
||||||
|
if distance_km <= tolerance_km:
|
||||||
|
# Direct hit - full points + bonus
|
||||||
|
base_score = 1000 + DIRECT_HIT_BONUS # 1000 + 200 bonus
|
||||||
|
is_correct = True
|
||||||
|
else:
|
||||||
|
if distance_km > MAX_DISTANCE_KM:
|
||||||
|
# Beyond max distance - 0 points
|
||||||
|
base_score = 0
|
||||||
|
is_correct = False
|
||||||
|
else:
|
||||||
|
# Non-linear decrease using exponential falloff
|
||||||
|
# This creates a steeper drop-off at the beginning
|
||||||
|
progress = (distance_km - tolerance_km) / (MAX_DISTANCE_KM - tolerance_km)
|
||||||
|
# Using a power function to create non-linear falloff
|
||||||
|
falloff = progress ** 1.5 # 1.5 makes the falloff steeper at the beginning
|
||||||
|
base_score = 1000 * (1 - falloff)
|
||||||
|
is_correct = True
|
||||||
|
base_score = max(0, min(1000, int(base_score)))
|
||||||
|
|
||||||
|
# Apply time penalty (reduces score based on time taken)
|
||||||
|
if is_correct and base_score > 0:
|
||||||
|
# Calculate time taken as a fraction of total time (0-1)
|
||||||
|
time_taken_sec = (int(current_question.time_per_question) * 1000 - time_remaining) / 1000
|
||||||
|
max_time_sec = int(current_question.time_per_question)
|
||||||
|
time_fraction = min(1.0, time_taken_sec / max_time_sec) if max_time_sec > 0 else 1.0
|
||||||
|
|
||||||
|
# Reduce points based on time taken (up to 50% reduction for answering at the last second)
|
||||||
|
time_penalty = base_score * 0.5 * time_fraction # Up to 50% penalty
|
||||||
|
final_score = max(1, base_score - time_penalty) # At least 1 point if correct
|
||||||
|
else:
|
||||||
|
final_score = base_score # 0 points for incorrect answers
|
||||||
|
|
||||||
|
score = int(round(final_score))
|
||||||
|
|
||||||
|
# Save the answer
|
||||||
|
answer_obj, created = QuizAnswer.objects.get_or_create(
|
||||||
|
participant=participant,
|
||||||
|
question_index=quiz_game.current_question_index,
|
||||||
|
defaults={
|
||||||
|
'answer_index': answer_index,
|
||||||
|
'is_correct': is_correct,
|
||||||
|
'score': score,
|
||||||
|
'time_remaining': time_remaining
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not created:
|
||||||
|
answer_obj.answer_index = answer_index
|
||||||
|
answer_obj.is_correct = is_correct
|
||||||
|
answer_obj.score = score
|
||||||
|
answer_obj.time_remaining = time_remaining
|
||||||
|
answer_obj.save()
|
||||||
|
|
||||||
|
participant.last_score = score
|
||||||
|
participant.score += score
|
||||||
|
participant.last_answer_correct = is_correct
|
||||||
|
participant.save()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except (QuizGameParticipant.DoesNotExist, IndexError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@database_sync_to_async
|
@database_sync_to_async
|
||||||
def save_answer(self, participant_id, answer, time_remaining):
|
def save_answer(self, participant_id, answer, time_remaining):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -125,3 +125,12 @@ div.qp-question-container {
|
|||||||
div.qp-question-container img {
|
div.qp-question-container img {
|
||||||
@apply max-w-[50vw] max-h-[40vh] mx-auto;
|
@apply max-w-[50vw] max-h-[40vh] mx-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div#mapquestion-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
div#map {
|
||||||
|
@apply md:w-[600px] lg:w-[900px] h-[400px]
|
||||||
|
}
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@tsparticles/confetti@3.0.3/tsparticles.confetti.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/@tsparticles/confetti@3.0.3/tsparticles.confetti.bundle.min.js"></script>
|
||||||
<title>qivip</title>
|
<title>qivip</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||||
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||||
|
crossorigin=""/>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-screen relative">
|
<body class="min-h-screen relative">
|
||||||
<div class="pb-64">
|
<div class="pb-64">
|
||||||
|
|||||||
@@ -98,9 +98,6 @@
|
|||||||
Eingabe
|
Eingabe
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}"
|
<a href="{% url 'library:new_question' %}?type=multiple_choice&quiz_id={{ quiz.id }}"
|
||||||
class=" mt-1 bg-blue-100 text-blue-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
|
class=" mt-1 bg-blue-100 text-blue-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
|
||||||
Multiple Choice
|
Multiple Choice
|
||||||
@@ -116,10 +113,15 @@
|
|||||||
Reihenfolge
|
Reihenfolge
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="{% url 'library:new_question' %}?type=guess&quiz_id={{ quiz.id }}"
|
<a href="{% url 'library:new_question' %}?type=guess&quiz_id={{ quiz.id }}"
|
||||||
class=" mt-1 bg-pink-100 text-pink-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
|
class=" mt-1 bg-pink-100 text-pink-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-blue-600 border-2">
|
||||||
Schätzen
|
Schätzen
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a href="{% url 'library:new_question' %}?type=map&quiz_id={{ quiz.id }}"
|
||||||
|
class=" mt-1 bg-purple-100 text-orange-800 px-4 py-2 rounded-md text-xs lg:text-lg hover:border-orange-600 border-2">
|
||||||
|
Karte
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,8 +142,14 @@
|
|||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<p class="font-medium">{{ question.data.question }}</p>
|
<p class="font-medium">{{ question.data.question }}</p>
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {% if question.data.type == 'multiple_choice' %}bg-blue-100 text-blue-800 {% elif question.data.type == 'input' %} bg-green-100 text-green-800 {% elif question.data.type == 'true_false' %}bg-purple-100 text-purple-800{% elif question.data.type == 'order' %}bg-yellow-100 text-yellow-800{% else %}bg-pink-100 text-pink-800 {% endif %}">
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {% if question.data.type == 'multiple_choice' %}bg-blue-100 text-blue-800 {% elif question.data.type == 'input' %} bg-green-100 text-green-800 {% elif question.data.type == 'map' %}bg-orange-100 text-orange-800 {% elif question.data.type == 'true_false' %}bg-purple-100 text-purple-800{% elif question.data.type == 'order' %}bg-yellow-100 text-yellow-800{% else %}bg-pink-100 text-pink-800 {% endif %}">
|
||||||
{% if question.data.type == 'multiple_choice' %}Multiple Choice{% elif question.data.type == 'input' %}Eingabe{% elif question.data.type == 'true_false' %}Wahr/Falsch{% elif question.data.type == 'order' %}Reihenfolge{% else %}Schätzen{% endif %}
|
{% if question.data.type == 'multiple_choice' %}Multiple Choice
|
||||||
|
{% elif question.data.type == 'input' %}Eingabe
|
||||||
|
{% elif question.data.type == 'map' %}Karte
|
||||||
|
{% elif question.data.type == 'true_false' %}Wahr/Falsch
|
||||||
|
{% elif question.data.type == 'order' %}Reihenfolge
|
||||||
|
{% else %}Schätzen
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
@@ -173,8 +181,46 @@
|
|||||||
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
|
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4 md:mt-0" />
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% elif question.data.type == 'map' and detail %}
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
<span class="font-medium">Zielort:</span>
|
||||||
|
<span id="target-location-{{ forloop.counter }}">
|
||||||
|
{% if question.data.target_location %}
|
||||||
|
{{ question.data.target_location }}
|
||||||
|
{% else %}
|
||||||
|
Kein Zielort festgelegt
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% if question.data.target_location %}
|
||||||
|
<script>
|
||||||
|
// Format the coordinates to 4 decimal places
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const targetEl = document.getElementById('target-location-{{ forloop.counter }}');
|
||||||
|
if (targetEl) {
|
||||||
|
const coords = targetEl.textContent.trim().split(',');
|
||||||
|
if (coords.length === 2) {
|
||||||
|
const lat = parseFloat(coords[0]).toFixed(4);
|
||||||
|
const lng = parseFloat(coords[1]).toFixed(4);
|
||||||
|
targetEl.textContent = `${lat}, ${lng}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
{% if question.data.tolerance_radius %}
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-300 mt-1">
|
||||||
|
<span class="font-medium">Toleranzradius:</span> {{ question.data.tolerance_radius }} Meter
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if question.image.image.url %}
|
||||||
|
<img src="{{ question.image.image.url }}" alt="Bild der Frage" class="w-[200px] rounded-lg shadow-md border-blue-600 border-2 rounded-xl shadow-md object-contain mt-4" />
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% elif question.data.type == 'order' %}
|
{% elif question.data.type == 'order' %}
|
||||||
<div class="w-full gap-4 md:flex justify-between ">
|
<div class="w-full gap-4 md:flex justify-between ">
|
||||||
<ul class="space-y-1 ">
|
<ul class="space-y-1 ">
|
||||||
{% for option in question.data.options|dictsort:"order" %}
|
{% for option in question.data.options|dictsort:"order" %}
|
||||||
<li class="flex items-center text-sm">
|
<li class="flex items-center text-sm">
|
||||||
@@ -188,7 +234,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% elif question.data.type == 'input' %}
|
{% elif question.data.type == 'input' %}
|
||||||
<div class="w-full gap-4 md:flex justify-between ">
|
<div class="w-full gap-4 md:flex justify-between ">
|
||||||
<ul class="space-y-1 ">
|
<ul class="space-y-1 ">
|
||||||
|
|||||||
131
django/templates/library/question/question_map.html
Normal file
131
django/templates/library/question/question_map.html
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||||
|
crossorigin=""></script>
|
||||||
|
<div id="mapquestion-container">
|
||||||
|
<div>
|
||||||
|
<form class="flex flex-col p-4 space-y-4" enctype="multipart/form-data" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="relative">
|
||||||
|
<label for="question" class="absolute -top-2 left-3 text-sm bg-white dark:bg-gray-900 px-1 text-blue-600 dark:text-white z-10">Frage</label>
|
||||||
|
<input type="text" id="question" name="question" class="p-4 w-full border-2 border-blue-400 rounded-md dark:text-white" value="{{ question.data.question }}" />
|
||||||
|
</div>
|
||||||
|
{% if image %}
|
||||||
|
<img src="{{ image.image.url }}" style="max-width: 100px;" alt="Bild der Frage">
|
||||||
|
<label for="reset_image" class="dark:text-white">
|
||||||
|
<input type="checkbox" name="reset_ques_image" id="reset_ques_image">
|
||||||
|
Bild zurücksetzen
|
||||||
|
</label>
|
||||||
|
{% endif %}
|
||||||
|
<div class="relative">
|
||||||
|
<label for="question_image" class="absolute -top-2 left-3 text-sm bg-white dark:bg-gray-900 px-1 text-blue-600 dark:text-white z-10">Bild der Frage</label>
|
||||||
|
<input class="p-4 w-full border-2 border-blue-400 rounded-md dark:text-white" type="file" name="question_image" id="question_image" accept="image/*">
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">Ziel auswählen</div>
|
||||||
|
<div id="map"></div>
|
||||||
|
<div class="relative">
|
||||||
|
<label for="location" class="absolute -top-2 left-3 text-sm bg-white dark:bg-gray-900 px-1 text-blue-600 dark:text-white z-10">Koordinaten</label>
|
||||||
|
<input type="text" id="location" name="location" class="p-4 w-full border-2 border-blue-400 rounded-md dark:text-white" value="{{ question.data.target_location }}" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<label for="time_per_question" class="absolute -top-2 left-3 text-sm bg-white dark:bg-gray-900 px-1 text-blue-600 dark:text-white z-10">Zeit pro Frage</label>
|
||||||
|
<select name="time_per_question" id="time_per_question" class="p-4 w-full border-2 border-blue-400 rounded-md bg-white dark:bg-gray-900 dark:text-gray-300">
|
||||||
|
<option value="10" {% if standard_time_per_question == "10" or time_per_question == "10" %}selected{% endif %}>10 Sekunden</option>
|
||||||
|
<option value="20" {% if standard_time_per_question == "20" or time_per_question == "20" %}selected{% endif %}>20 Sekunden</option>
|
||||||
|
<option value="30" {% if standard_time_per_question == "30" or time_per_question == "30" %}selected{% endif %}>30 Sekunden</option>
|
||||||
|
<option value="45" {% if standard_time_per_question == "45" or time_per_question == "45" %}selected{% endif %}>45 Sekunden</option>
|
||||||
|
<option value="60" {% if standard_time_per_question == "60" or time_per_question == "60" %}selected{% endif %}>1 Minute</option>
|
||||||
|
<option value="120" {% if standard_time_per_question == "120" or time_per_question == "120" %}selected{% endif %}>2 Minuten</option>
|
||||||
|
<option value="300" {% if standard_time_per_question == "300" or time_per_question == "300" %}selected{% endif %}>5 Minuten</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<label for="credits" class="absolute -top-2 left-3 text-sm bg-white dark:bg-gray-900 px-1 text-blue-600 dark:text-white z-10">Credits</label>
|
||||||
|
<textarea name="credits" id="credits" rows="4" class="p-4 w-full border-2 border-blue-400 rounded-md dark:text-white">{{ credits }}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="relative">
|
||||||
|
<label for="tolerance_radius" class="absolute -top-2 left-3 text-sm bg-white dark:bg-gray-900 px-1 text-blue-600 dark:text-white z-10">Radius (Meter) für volle Punktzahl</label>
|
||||||
|
<input type="number" id="tolerance_radius" name="tolerance_radius" class="p-4 w-full border-2 border-blue-400 rounded-md dark:text-white" min="1" value="{{ question.data.tolerance_radius }}" />
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between mt-4">
|
||||||
|
<button class="p-2 rounded-md text-white bg-gray-600 hover:bg-gray-700" type="button" onclick="window.history.back()">Zurück</button>
|
||||||
|
<input class="p-2 rounded-md text-white bg-blue-400 hover:bg-blue-500" type="submit" value="Speichern">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let marker = null;
|
||||||
|
let circle = null;
|
||||||
|
const locationInput = document.getElementById('location');
|
||||||
|
const radiusInput = document.getElementById('tolerance_radius');
|
||||||
|
|
||||||
|
const map = L.map('map').setView([51.72, 10.45], 3);
|
||||||
|
|
||||||
|
// Prüfe, ob Dark Mode aktiv ist
|
||||||
|
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
// Wähle das passende Karten-Design
|
||||||
|
const tileUrl = isDarkMode
|
||||||
|
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}.png'
|
||||||
|
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}.png';
|
||||||
|
|
||||||
|
L.tileLayer(tileUrl, {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© OpenStreetMap © CARTO'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
function updateCircle(latlng) {
|
||||||
|
const radius = parseInt(radiusInput.value) || 100;
|
||||||
|
if (circle) {
|
||||||
|
map.removeLayer(circle);
|
||||||
|
}
|
||||||
|
circle = L.circle(latlng, {
|
||||||
|
color: 'blue',
|
||||||
|
fillColor: '#87CEEB',
|
||||||
|
fillOpacity: 0.2,
|
||||||
|
radius: radius
|
||||||
|
}).addTo(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateInput(latlng) {
|
||||||
|
locationInput.value = `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`;
|
||||||
|
if (marker) {
|
||||||
|
updateCircle(marker.getLatLng());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeOrMoveMarker(latlng) {
|
||||||
|
if (marker) {
|
||||||
|
marker.setLatLng(latlng);
|
||||||
|
} else {
|
||||||
|
marker = L.marker(latlng, { draggable: true }).addTo(map);
|
||||||
|
|
||||||
|
marker.on('dragend', () => {
|
||||||
|
updateInput(marker.getLatLng());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateInput(latlng);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update circle when radius changes
|
||||||
|
radiusInput.addEventListener('input', () => {
|
||||||
|
if (marker) {
|
||||||
|
updateCircle(marker.getLatLng());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Place marker at existing coordinates if available
|
||||||
|
const existingCoords = '{{ question.data.target_location }}';
|
||||||
|
if (existingCoords) {
|
||||||
|
const [lat, lng] = existingCoords.split(',').map(Number);
|
||||||
|
placeOrMoveMarker({ lat, lng });
|
||||||
|
map.setView([lat, lng], 6); // Zoom in to show the marker
|
||||||
|
}
|
||||||
|
|
||||||
|
map.on('click', e => placeOrMoveMarker(e.latlng));
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -50,8 +50,7 @@
|
|||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
|
{% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-4" id="options-container">
|
<div class="grid grid-cols-2 gap-4" id="options-container">
|
||||||
{% for option in question_data.options %}
|
{% for option in question_data.options %}
|
||||||
<button
|
<button
|
||||||
@@ -162,8 +161,93 @@ class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-
|
|||||||
Abschicken
|
Abschicken
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
|
||||||
{% elif question_data.type == "guess" %}
|
{% endif %}
|
||||||
|
{% elif question_data.type == "map" and request.session.mode == "learn" %}
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||||
|
crossorigin=""></script>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">Ziel auswählen</div>
|
||||||
|
<div id="map" class="h-96"></div>
|
||||||
|
<input type="hidden" value="" id="singleanswer" name="singleanswer">
|
||||||
|
<div class="mt-4">
|
||||||
|
<button onclick="submitAnswerMap()" class="bg-blue-400 text-white px-4 py-2 rounded-md hover:bg-blue-500 transition-colors">
|
||||||
|
Abgeben
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="location" name="location" value="">
|
||||||
|
<script>
|
||||||
|
let marker = null;
|
||||||
|
const locationInput = document.getElementById('location');
|
||||||
|
const map = L.map('map').setView([51.72, 10.45], 3);
|
||||||
|
|
||||||
|
// Prüfe, ob Dark Mode aktiv ist
|
||||||
|
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
// Wähle das passende Karten-Design
|
||||||
|
const tileUrl = isDarkMode
|
||||||
|
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_nolabels/{z}/{x}/{y}.png'
|
||||||
|
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_nolabels/{z}/{x}/{y}.png';
|
||||||
|
|
||||||
|
L.tileLayer(tileUrl, {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© OpenStreetMap © CARTO'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
function updateInput(latlng) {
|
||||||
|
locationInput.value = `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeOrMoveMarker(latlng) {
|
||||||
|
if (marker) {
|
||||||
|
marker.setLatLng(latlng);
|
||||||
|
} else {
|
||||||
|
marker = L.marker(latlng, { draggable: true }).addTo(map);
|
||||||
|
marker.on('dragend', () => {
|
||||||
|
updateInput(marker.getLatLng());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateInput(latlng);
|
||||||
|
}
|
||||||
|
|
||||||
|
map.on('click', e => placeOrMoveMarker(e.latlng));
|
||||||
|
|
||||||
|
function submitAnswerMap() {
|
||||||
|
const answer = document.getElementById("location").value;
|
||||||
|
|
||||||
|
if (answer === "") {
|
||||||
|
alert("Bitte einen Ort auf der Karte auswählen!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add grey overlay
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.style.position = 'fixed';
|
||||||
|
overlay.style.top = '0';
|
||||||
|
overlay.style.left = '0';
|
||||||
|
overlay.style.width = '100%';
|
||||||
|
overlay.style.height = '100%';
|
||||||
|
overlay.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
|
||||||
|
overlay.style.zIndex = '1000';
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
overlay.style.justifyContent = 'center';
|
||||||
|
overlay.style.alignItems = 'center';
|
||||||
|
overlay.style.color = 'white';
|
||||||
|
overlay.style.fontSize = '24px';
|
||||||
|
overlay.style.fontWeight = 'bold';
|
||||||
|
overlay.textContent = 'Antwort übermittelt';
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
|
||||||
|
|
||||||
|
// Submit the answer
|
||||||
|
submitAnswer(answer);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% elif question_data.type == "map" and request.session.mode != "learn" %}
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400 mb-2 text-center">Bitte den gesuchten Ort auf der Karte auswählen</p>
|
||||||
|
{% elif question_data.type == "guess" %}
|
||||||
{% if request.session.mode == "learn" %}
|
{% if request.session.mode == "learn" %}
|
||||||
<form class="flex flex-col items-center gap-4 mt-4">
|
<form class="flex flex-col items-center gap-4 mt-4">
|
||||||
<!-- Slider mit Beschriftung -->
|
<!-- Slider mit Beschriftung -->
|
||||||
@@ -193,11 +277,7 @@ class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-semibold rounded-
|
|||||||
|
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -25,8 +25,7 @@
|
|||||||
<div class="w-full bg-gray-300 rounded-full h-4 my-4">
|
<div class="w-full bg-gray-300 rounded-full h-4 my-4">
|
||||||
<div id="time-bar" class="bg-blue-600 h-4 rounded-full transition-all duration-100 linear" style="width: 100%;"></div>
|
<div id="time-bar" class="bg-blue-600 h-4 rounded-full transition-all duration-100 linear" style="width: 100%;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
|
||||||
{% if question_data.type == "multiple_choice" or question_data.type == "true_false" %}
|
|
||||||
<div class="grid grid-cols-2 gap-4" id="options-container">
|
<div class="grid grid-cols-2 gap-4" id="options-container">
|
||||||
{% for option in question_data.options %}
|
{% for option in question_data.options %}
|
||||||
<button data-index="{{ forloop.counter0 }}"
|
<button data-index="{{ forloop.counter0 }}"
|
||||||
@@ -168,6 +167,58 @@
|
|||||||
Abschicken
|
Abschicken
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% elif question_data.type == "map" %}
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||||
|
crossorigin=""></script>
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-400 mb-2">Ziel auswählen</div>
|
||||||
|
<div id="map" class="h-96"></div>
|
||||||
|
<input type="hidden" value="" id="location" name="location">
|
||||||
|
<div class="mt-4">
|
||||||
|
<button onclick="submitAnswerMap()" class="bg-blue-400 text-white px-4 py-2 rounded-md hover:bg-blue-500 transition-colors">
|
||||||
|
Abgeben
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let marker = null;
|
||||||
|
const locationInput = document.getElementById('location');
|
||||||
|
|
||||||
|
const map = L.map('map').setView([51.72, 10.45], 3);
|
||||||
|
|
||||||
|
// Prüfe, ob Dark Mode aktiv ist
|
||||||
|
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
// Wähle das passende Karten-Design
|
||||||
|
const tileUrl = isDarkMode
|
||||||
|
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_nolabels/{z}/{x}/{y}.png'
|
||||||
|
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_nolabels/{z}/{x}/{y}.png';
|
||||||
|
|
||||||
|
L.tileLayer(tileUrl, {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© OpenStreetMap © CARTO'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
function updateInput(latlng) {
|
||||||
|
locationInput.value = `${latlng.lat.toFixed(6)},${latlng.lng.toFixed(6)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function placeOrMoveMarker(latlng) {
|
||||||
|
if (marker) {
|
||||||
|
marker.setLatLng(latlng);
|
||||||
|
} else {
|
||||||
|
marker = L.marker(latlng, { draggable: true }).addTo(map);
|
||||||
|
|
||||||
|
marker.on('dragend', () => {
|
||||||
|
updateInput(marker.getLatLng());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateInput(latlng);
|
||||||
|
}
|
||||||
|
|
||||||
|
map.on('click', e => placeOrMoveMarker(e.latlng));
|
||||||
|
</script>
|
||||||
|
{% else %}
|
||||||
|
<p>aaaaah gibts nicht</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -203,7 +254,22 @@
|
|||||||
wsUtil.handleClose(e);
|
wsUtil.handleClose(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
function submitAnswer(optionIndex) {
|
function submitAnswer(optionIndex=0, has_options=true) {
|
||||||
|
if (!has_options){
|
||||||
|
answer = document.getElementById("singleanswer").value;
|
||||||
|
|
||||||
|
if (answer == ""){
|
||||||
|
alert("Bitte eine Antwort eingeben!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gameSocket.send(JSON.stringify({
|
||||||
|
type: 'submit_answer',
|
||||||
|
participant_id: participantId,
|
||||||
|
answer:answer,
|
||||||
|
time_remaining: questionDuration
|
||||||
|
}));
|
||||||
|
}
|
||||||
if (hasAnswered) return;
|
if (hasAnswered) return;
|
||||||
let input = ""; // Declare input here
|
let input = ""; // Declare input here
|
||||||
|
|
||||||
@@ -249,7 +315,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function submitAnswerMap() {
|
||||||
|
answer = document.getElementById("location").value;
|
||||||
|
|
||||||
|
if (answer == ""){
|
||||||
|
alert("Bitte eine Antwort eingeben!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add grey overlay
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.style.position = 'fixed';
|
||||||
|
overlay.style.top = '0';
|
||||||
|
overlay.style.left = '0';
|
||||||
|
overlay.style.width = '100%';
|
||||||
|
overlay.style.height = '100%';
|
||||||
|
overlay.style.backgroundColor = 'rgba(128, 128, 128, 0.5)';
|
||||||
|
overlay.style.zIndex = '1000';
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
overlay.style.justifyContent = 'center';
|
||||||
|
overlay.style.alignItems = 'center';
|
||||||
|
overlay.style.color = 'white';
|
||||||
|
overlay.style.fontSize = '24px';
|
||||||
|
overlay.style.fontWeight = 'bold';
|
||||||
|
overlay.textContent = 'Antwort übermittelt';
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const timeRemaining = Math.max(0, questionDuration - (now - questionStartTime));
|
||||||
|
|
||||||
|
gameSocket.send(JSON.stringify({
|
||||||
|
type: 'submit_answer',
|
||||||
|
participant_id: participantId,
|
||||||
|
answer: answer,
|
||||||
|
time_remaining: timeRemaining
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Timer
|
// Timer
|
||||||
function updateTimer() {
|
function updateTimer() {
|
||||||
|
|||||||
@@ -9,9 +9,100 @@
|
|||||||
|
|
||||||
<h2 class="text-xl font-bold mb-4">Aktuelle Frage</h2>
|
<h2 class="text-xl font-bold mb-4">Aktuelle Frage</h2>
|
||||||
<p class="text-lg mb-2">{{ question_data.question }}</p>
|
<p class="text-lg mb-2">{{ question_data.question }}</p>
|
||||||
{% if question_data.type != "order" and question_data.type != "guess" %}
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
|
|
||||||
|
{% if question_data.type == "map" %}
|
||||||
|
<!-- Map Question Display -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<div id="map" class="h-96 w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||||
|
crossorigin=""></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Parse the target location from the string format "lat,lng"
|
||||||
|
const targetCoords = '{{ question_data.target_location|escapejs }}'.split(',');
|
||||||
|
let targetLat, targetLng;
|
||||||
|
|
||||||
|
if (targetCoords.length === 2) {
|
||||||
|
targetLat = parseFloat(targetCoords[0].trim());
|
||||||
|
targetLng = parseFloat(targetCoords[1].trim());
|
||||||
|
|
||||||
|
// Normalize longitude to be within -180 to 180
|
||||||
|
targetLng = ((targetLng % 360) + 540) % 360 - 180;
|
||||||
|
|
||||||
|
// Clamp latitude to valid range (-90 to 90)
|
||||||
|
targetLat = Math.max(-90, Math.min(90, targetLat));
|
||||||
|
|
||||||
|
console.log('Normalized coordinates:', { lat: targetLat, lng: targetLng });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate coordinates
|
||||||
|
if (isNaN(targetLat) || isNaN(targetLng)) {
|
||||||
|
console.warn('Invalid coordinates, hiding map');
|
||||||
|
document.getElementById('map-container').style.display = 'none';
|
||||||
|
return; // Exit the function early
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetLocation = [targetLat, targetLng];
|
||||||
|
console.log('Using coordinates:', targetLocation);
|
||||||
|
|
||||||
|
// Initialize map centered on target location
|
||||||
|
const map = L.map('map').setView(targetLocation, 7);
|
||||||
|
|
||||||
|
// Check for dark mode
|
||||||
|
const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
// Choose appropriate map style
|
||||||
|
const tileUrl = isDarkMode
|
||||||
|
? 'https://{s}.basemaps.cartocdn.com/rastertiles/dark_all/{z}/{x}/{y}.png'
|
||||||
|
: 'https://{s}.basemaps.cartocdn.com/rastertiles/light_all/{z}/{x}/{y}.png';
|
||||||
|
|
||||||
|
L.tileLayer(tileUrl, {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© OpenStreetMap © CARTO',
|
||||||
|
noWrap: true
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add target marker
|
||||||
|
L.marker(targetLocation, {
|
||||||
|
icon: L.divIcon({
|
||||||
|
html: '🎯',
|
||||||
|
className: 'text-3xl',
|
||||||
|
iconSize: [32, 32],
|
||||||
|
iconAnchor: [16, 32],
|
||||||
|
popupAnchor: [0, -32]
|
||||||
|
})
|
||||||
|
}).addTo(map)
|
||||||
|
.bindPopup('Zielort')
|
||||||
|
.openPopup();
|
||||||
|
|
||||||
|
// Add tolerance radius circle if available
|
||||||
|
{% if question_data.tolerance_radius %}
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
const toleranceRadius = parseFloat('{{ question_data.tolerance_radius|escapejs }}'.replace(',', '.'));
|
||||||
|
if (!isNaN(toleranceRadius) && toleranceRadius > 0) {
|
||||||
|
L.circle(targetLocation, {
|
||||||
|
color: isDarkMode ? '#3b82f6' : '#2563eb',
|
||||||
|
fillColor: isDarkMode ? '#3b82f6' : '#2563eb',
|
||||||
|
fillOpacity: 0.2,
|
||||||
|
radius: toleranceRadius
|
||||||
|
}).addTo(map);
|
||||||
|
console.log('Added tolerance radius:', toleranceRadius);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error adding tolerance radius:', e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
{% endif %}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% elif question_data.type != "order" and question_data.type != "guess" %}
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
{% for option in question_data.options %}
|
{% for option in question_data.options %}
|
||||||
|
|
||||||
<div id="option-box-{{ forloop.counter0 }}" class=" dark:bg-transparent relative p-4 rounded-lg overflow-auto break-words hyphens-auto {% if option.is_correct %} border-2 border-green-500{% else %}border-2 border-red-500{% endif %}">
|
<div id="option-box-{{ forloop.counter0 }}" class=" dark:bg-transparent relative p-4 rounded-lg overflow-auto break-words hyphens-auto {% if option.is_correct %} border-2 border-green-500{% else %}border-2 border-red-500{% endif %}">
|
||||||
@@ -25,7 +116,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% elif question_data.type == "guess" %}
|
{% elif question_data.type == "guess" %}
|
||||||
<div id="option-box-0" class="relative p-4 rounded-lg border-2 border-green-500 overflow-auto">
|
<div id="option-box-0" class="relative p-4 rounded-lg border-2 border-green-500 overflow-auto">
|
||||||
<div id="option-fill-0" class="absolute left-0 top-0 h-full bg-green-300 opacity-50 z-0 transition-all duration-1500" style="width: 0%;"></div>
|
<div id="option-fill-0" class="absolute left-0 top-0 h-full bg-green-300 opacity-50 z-0 transition-all duration-1500" style="width: 0%;"></div>
|
||||||
<div class="relative z-10">
|
<div class="relative z-10">
|
||||||
|
|||||||
@@ -12,3 +12,5 @@ qrcode~=7.4.2
|
|||||||
reportlab~=4.2.0
|
reportlab~=4.2.0
|
||||||
pip-licenses ~= 5.0.0
|
pip-licenses ~= 5.0.0
|
||||||
requests
|
requests
|
||||||
|
requests
|
||||||
|
geopy
|
||||||
|
|||||||
Reference in New Issue
Block a user