1
0
forked from orson/bachemap

started implementing the integrated camera function and the PWA

This commit is contained in:
Orson 2025-03-14 00:48:49 -06:00
parent ea0b967366
commit e6e6620eb7
10 changed files with 323 additions and 8 deletions

28
app.py
View File

@ -205,7 +205,9 @@ def create_app(config=Config):
else:
return redirect(url_for('index'))
@app.route('/manifest.json')
def manifest():
return send_from_directory('static', 'manifest.json')
@app.route('/dashboard')
@login_required
@ -241,7 +243,31 @@ def create_app(config=Config):
}
mongo.db.users.insert_one(admin_user)
print(f"Admin {username} added! Referral code is {admin_user['referral_code']}")
@app.route("/camera", methods=["GET", "POST"])
@login_required
def camera():
if request.method == "POST":
# 1. Grab geolocation data from the form
latitude = request.form.get("latitude")
longitude = request.form.get("longitude")
# 2. Grab the photo file
photo = request.files.get("photo")
# 3. Save the file if it exists
if photo:
# Make sure your 'uploads' folder exists or handle dynamically
photo_path = os.path.join(app.config['UPLOAD_FOLDER'], photo.filename)
photo.save(photo_path)
# TODO: Optionally embed latitude/longitude into the photos EXIF here
# using piexif or Pillow
# For demonstration, just redirect or return a success message
return f"Photo uploaded! Lat: {latitude}, Lng: {longitude}"
# If GET request, just render the camera page
return render_template("camera.html")
@app.route('/leaderboard')
def leaderboard():
pipeline = [

BIN
static/images/contract.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
static/images/contract2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

21
static/manifest.json Normal file
View File

@ -0,0 +1,21 @@
{
"name": "El glorioso y nunca bien ponderado Bachemapa",
"short_name": "Bachemapa",
"start_url": "/camera",
"scope": "/",
"icons": [
{
"src": "/static/images/contract.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/static/images/contract2.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffd31d",
"background_color": "#333",
"display": "standalone"
}

42
static/service-worker.js Normal file
View File

@ -0,0 +1,42 @@
const CACHE_NAME = 'bachemapa-pwa-v2';
const urlsToCache = [
'/',
'/quienes',
'/dashboard',
'/leaderboard',
'/thelogin',
'/static/css/main.css',
'/static/js/main.js',
'/static/js/leaflet.js',
'/static/css/styles.css',
'/static/images/marker-icon.png',
'/static/images/marker-shadow.png',
'/static/images/leaflet.css',
'/static/images/contract.png',
'/static/images/contract2.png',
'/static/manifest.json',
'/manifest.json',
'/static/service-worker.js'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log('Opened cache');
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Return cached asset if available, otherwise fetch from network
return response || fetch(event.request);
})
);
});

View File

@ -302,7 +302,19 @@ section#pinner-modal::-webkit-scrollbar-thumb {
.eaflet-popup-content {
width: 100%;
}
.control-button {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
padding: 15px 30px;
background-color: rgba(255, 255, 255, 0.7);
border-radius: 50px;
border: none;
font-size: 18px;
z-index: 100;
color: black;
}
.marker-cluster {
background-color: rgb(249, 170, 61) !important;
border-radius: 100px;

View File

@ -21,6 +21,7 @@
<script type="importmap" src="{{ url_for('static', filename='leaflet.markercluster.js.map')}}"></script>
<link href="{{ url_for('static', filename='MarkerCluster.css')}}">
<link href="{{ url_for('static', filename='MarkerCluster.Default.css')}}">
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<meta name="description" content="Bachemapa es una plataforma dedicada a documentar y reportar problemas con el desarrollo urbano en Querétaro, México. Encuentra y aporta información actualizada sobre baches, deficiencias en infraestructura y más.">
@ -43,6 +44,8 @@
<!-- Otros meta tags -->
<link rel="canonical" href="https://baches.qro.mx">
{% block head %}
{% endblock %}
<!-- Versión pública 0.1 -->
</head>
@ -58,7 +61,7 @@
</div>
{% endif %}
{% endwith %}
<main class="container-fluid" style="padding:0px;height: 100vh; width:100vw; z-index: 5; position: absolute;">
<main class="container-fluid" style="padding:0px;height: 100vh; width:100vw; z-index: 5; position: static;">
{% block content %}
@ -115,5 +118,18 @@ window.addEventListener('load', function() {
pinnerDesktop.addEventListener('click', toggleSlide);
}
});
</script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/static/service-worker.js')
.then(function(registration) {
console.log('Service Worker registered with scope:', registration.scope);
}, function(err) {
console.log('Service Worker registration failed:', err);
});
});
}
</script>
</html>

185
templates/camera.html Normal file
View File

@ -0,0 +1,185 @@
{% extends 'base.html' %}
{% block head %}
<title>Take a Photo with Geolocation</title>
<link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
<style>
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
#camera-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#video {
width: 100%;
height: 100%;
object-fit: cover;
}
#canvas {
display: none;
width: 100%;
height: 100%;
object-fit: cover;
}
#file-fallback {
display: none;
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
background: rgba(255, 255, 255, 0.8);
padding: 10px;
border-radius: 10px;
color: black;
}
</style>
{% endblock %}
{% block content %}
<div id="camera-container">
<video id="video" autoplay playsinline></video>
<canvas id="canvas"></canvas>
</div>
<div style="position: absolute; bottom: 0; width: 100%; display: flex; flex-direction: column; padding: 10px; box-sizing: border-box;">
<button id="capture-btn" class="control-button" style="margin: 10px;">Tomar foto</button>
<div style="display: flex; justify-content: space-between; width: 100%; box-sizing: border-box;">
<button id="retake-btn" class="control-button" style="display: none; margin: 10px 5px 10px 10px; flex: 1; left:20%; background-color: rgba(180,0,0,0.6);">Nueva foto</button>
<button id="send-btn" class="control-button" style="display: none; margin: 10px 10px 10px 5px; flex: 1;">Enviar foto</button>
</div>
</div>
<form id="camera-form" action="{{ url_for('camera') }}" method="POST" enctype="multipart/form-data" style="display:none;">
<input type="hidden" name="latitude" id="latitude" value="">
<input type="hidden" name="longitude" id="longitude" value="">
<input type="hidden" name="has_image" id="has_image" value="false">
<!-- Fallback file input for unsupported browsers -->
<div id="file-fallback">
<label for="photo">Take/Choose a Photo:</label>
<input type="file" name="photo" id="photo" accept="image/*" capture="environment" required>
<button type="submit">Upload</button>
</div>
</form>
<script>
// Get DOM elements
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
const captureBtn = document.getElementById('capture-btn');
const retakeBtn = document.getElementById('retake-btn');
const sendBtn = document.getElementById('send-btn');
const form = document.getElementById('camera-form');
const fallbackInput = document.getElementById('file-fallback');
const hasImageInput = document.getElementById('has_image');
// Set up camera stream
async function setupCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment',
width: { ideal: window.innerWidth },
height: { ideal: window.innerHeight }
},
audio: false
});
video.srcObject = stream;
} catch (err) {
console.error('Error accessing camera:', err);
fallbackInput.style.display = 'block';
}
}
// Initialize camera if supported
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
setupCamera();
} else {
console.error('getUserMedia not supported');
fallbackInput.style.display = 'block';
}
// Capture photo from video feed
captureBtn.addEventListener('click', function() {
const context = canvas.getContext('2d');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
context.drawImage(video, 0, 0, canvas.width, canvas.height);
// Stop camera stream
video.srcObject.getTracks().forEach(track => track.stop());
video.style.display = 'none';
canvas.style.display = 'block';
// Update controls
captureBtn.style.display = 'none';
retakeBtn.style.display = 'inline-block';
sendBtn.style.display = 'inline-block';
// Mark that we have an image
hasImageInput.value = 'true';
});
// Retake photo
retakeBtn.addEventListener('click', function() {
canvas.style.display = 'none';
video.style.display = 'block';
captureBtn.style.display = 'inline-block';
retakeBtn.style.display = 'none';
sendBtn.style.display = 'none';
hasImageInput.value = 'false';
setupCamera();
});
// Send photo
sendBtn.addEventListener('click', function() {
if (hasImageInput.value === 'true') {
// Convert canvas to blob and append to FormData
canvas.toBlob(function(blob) {
const formData = new FormData(form);
formData.delete('photo'); // Remove any file input value
formData.append('photo', blob, 'camera-capture.jpg');
// Submit form data via fetch
fetch(form.action, {
method: 'POST',
body: formData
}).then(response => {
if (response.redirected) {
window.location.href = response.url;
}
}).catch(error => console.error('Error:', error));
}, 'image/jpeg', 0.9);
}
});
// Request Geolocation Permission and Set Form Fields
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
(position) => {
document.getElementById('latitude').value = position.coords.latitude;
document.getElementById('longitude').value = position.coords.longitude;
},
(error) => {
console.error('Error getting location:', error);
},
{
enableHighAccuracy: true,
timeout: 5000
}
);
} else {
console.error('Geolocation not supported in this browser.');
}
</script>
{% endblock %}

View File

@ -72,6 +72,7 @@
// Error handling for geolocation
function onLocationError(e) {
alert(e.message);
map.setView([20.57, -100.38], 13); // Default to Querétaro's coordinates
}
// Use Leaflet's built-in location detection
@ -102,4 +103,10 @@
user_radial.remove();
});
</script>
{% if current_user.is_authenticated %}
<div style="position: absolute; bottom: 1rem; left: 50%; transform: translateX(-50%); z-index: 999;">
<button class="control-button"><a href="/camera" class="btn btn-primary">Tomar foto</a></button>
</div>
{% endif %}
{% endblock %}

View File

@ -3,7 +3,7 @@
<a href="/">
<li style="display: flex; align-items: center;">
<img id="logo" src="{{ url_for('static', filename='images/bachemapalogo.png') }}" alt="Bachemapa Logo" style="height: 10vh; margin-right: 10px;">
<h3 style="margin-bottom: 0;"><span style="color: #4a8522;">Bache</span><span style="color: #fa8721;">mapa</span></h3>
<h3 style="margin-bottom: 0;" id="sitename"><span style="color: #4a8522;">Bache</span><span style="color: #fa8721;">mapa</span></h3>
</li>
</a>
</ul>
@ -25,6 +25,7 @@
{% endif %}
<li><a href="/quienes" role="button" class="secondary">Somos</a></li>
<li><a href="/leaderboard" role="button">Bachistas ★</a></li>
<li><a style="border-radius: 100%; width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; margin-right: 10px; text-decoration: none;" href="/quienes#tengopreguntas"><i style="color:white" class="fas fa-question-circle"></i></a></li>
</ul>
</details>
@ -39,22 +40,27 @@
{% endif %}
<li><a href="/quienes" role="button" class="secondary">Somos</a></li>
<li><a href="/leaderboard" role="button">Bachistas ★</a></li>
</ul>
</ul>
<ul class="desktop-menu">
<li><a style="border-radius: 100%; width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; margin-right: 10px; text-decoration: none;" href="/quienes#tengopreguntas"><i style="color:white" class="fas fa-question-circle"></i></a></li>
</ul>
</ul>
<style>
@media (max-width: 715px) {
.desktop-menu {
display: none;
}
#sitename {
display: none;
}
}
@media (min-width: 716px) {
.mobile-menu {
display: none;
}
#sitename {
display: block;
}
}
</style>