add floor plan

This commit is contained in:
Yan Zhou 2025-06-12 18:53:02 +02:00
parent 5935064606
commit 5d689e6005
5 changed files with 254 additions and 41 deletions

View file

@ -5,33 +5,59 @@ import type { PageServerLoad } from "./$types";
import { connect } from "mqtt"; import { connect } from "mqtt";
export const load: PageServerLoad = async ({ params }) => { export const load: PageServerLoad = async ({ params }) => {
{ // Convert slug to number for floor lookup
const floor_cnt = await db.select({ floor: table.plans.floor, json: table.plans.plan }).from(table.plans).where(eq(table.plans.floor, params.slug)); const floorNumber = Number(params.slug);
if (floor_cnt.length == 0) {
await db.insert(table.plans).values({ floor: params.slug, plan: { // First check if we have a saved floor configuration in the floors table
"regions": [ const floorData = await db.select({
{ "start": { "x": 100, "y": 100 }, "end": { "x": 400, "y": 100 } }, floor: table.floors.floor,
{ "start": { "x": 400, "y": 100 }, "end": { "x": 400, "y": 300 } }, url: table.floors.url
{ "start": { "x": 400, "y": 300 }, "end": { "x": 100, "y": 300 } }, }).from(table.floors).where(eq(table.floors.floor, floorNumber));
{ "start": { "x": 100, "y": 300 }, "end": { "x": 100, "y": 100 } }
], if (floorData.length > 0 && floorData[0].url && floorData[0].url !== "/") {
"doors": [ try {
{ "location": { "x": 240, "y": 100 }, "width": 50, "rotation": 0 } // Try to parse the saved configuration
], const config = JSON.parse(floorData[0].url);
"furnitures": [ return {
{ slug: params.slug,
"minBound": { "x": 150, "y": 150 }, floorConfig: config,
"maxBound": { "x": 200, "y": 200 }, hasConfig: true
"equipName": "Table", };
"xPlacement": 150, } catch (e) {
"yPlacement": 150, console.error("Error parsing floor configuration:", e);
"rotation": 0
}
]
}});
} }
} }
// Fallback to the old canvas drawing system
const floor_cnt = await db.select({ floor: table.plans.floor, json: table.plans.plan }).from(table.plans).where(eq(table.plans.floor, params.slug));
if (floor_cnt.length == 0) {
await db.insert(table.plans).values({ floor: params.slug, plan: {
"regions": [
{ "start": { "x": 100, "y": 100 }, "end": { "x": 400, "y": 100 } },
{ "start": { "x": 400, "y": 100 }, "end": { "x": 400, "y": 300 } },
{ "start": { "x": 400, "y": 300 }, "end": { "x": 100, "y": 300 } },
{ "start": { "x": 100, "y": 300 }, "end": { "x": 100, "y": 100 } }
],
"doors": [
{ "location": { "x": 240, "y": 100 }, "width": 50, "rotation": 0 }
],
"furnitures": [
{
"minBound": { "x": 150, "y": 150 },
"maxBound": { "x": 200, "y": 200 },
"equipName": "Table",
"xPlacement": 150,
"yPlacement": 150,
"rotation": 0
}
]
}});
}
const floor_ = await db.select({ floor: table.plans.floor, json: table.plans.plan }).from(table.plans).where(eq(table.plans.floor, params.slug)); const floor_ = await db.select({ floor: table.plans.floor, json: table.plans.plan }).from(table.plans).where(eq(table.plans.floor, params.slug));
return { slug: params.slug, floor: floor_ }; return {
slug: params.slug,
floor: floor_,
hasConfig: false
};
}; };

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
import { Thermometer, Droplets, Gauge, Mountain, Cpu } from "@lucide/svelte";
const { data } = $props(); const { data } = $props();
@ -37,7 +38,7 @@
} }
}); });
let canvasEl: HTMLCanvasElement; let canvasEl = $state<HTMLCanvasElement>();
let ctx; let ctx;
function drawFloorplan(data) { function drawFloorplan(data) {
@ -81,14 +82,68 @@
ctx.stroke(); ctx.stroke();
}); });
} }
onMount(() => { onMount(() => {
ctx = canvasEl.getContext("2d"); if (!data.hasConfig && data.floor?.[0]?.json) {
drawFloorplan(data.floor[0].json); ctx = canvasEl.getContext("2d");
drawFloorplan(data.floor[0].json);
}
}); });
</script> </script>
<div style="display: flex; justify-content: center; align-items: center; height: 100%;"> <div class="container mx-auto p-6">
<h1 class="text-center text-4xl font-bold">Floor {data.slug}</h1> <h1 class="mb-6 text-center text-4xl font-bold">Floor {data.slug}</h1>
<span>{mqttMessage}</span>
<canvas bind:this={canvasEl} class="obj-contain" width="800%" height="600%"></canvas> {#if data.hasConfig && data.floorConfig}
<!-- Display saved floor plan configuration -->
<div class="relative mx-auto max-w-4xl">
{#if data.floorConfig.image}
<div class="relative overflow-hidden rounded-lg border bg-gray-50">
<img src={data.floorConfig.image} alt="Floor plan" class="h-auto w-full" />
{#if data.floorConfig.devices}
{#each data.floorConfig.devices as device}
<div
class="absolute -translate-x-1/2 -translate-y-1/2 transform rounded-lg border-2 border-blue-500 bg-white p-3 shadow-lg"
style="left: {device.x}%; top: {device.y}%;"
>
<div class="flex items-center gap-2">
<Cpu class="h-5 w-5 text-blue-600" />
<span class="text-xs font-medium">{device.name}</span>
</div>
<div class="mt-2 grid grid-cols-2 gap-1">
<div class="flex items-center gap-1">
<Thermometer class="h-3 w-3 text-orange-500" />
<span class="text-xs">23°C</span>
</div>
<div class="flex items-center gap-1">
<Droplets class="h-3 w-3 text-blue-500" />
<span class="text-xs">45%</span>
</div>
<div class="flex items-center gap-1">
<Gauge class="h-3 w-3 text-green-500" />
<span class="text-xs">1013</span>
</div>
<div class="flex items-center gap-1">
<Mountain class="h-3 w-3 text-purple-500" />
<span class="text-xs">120m</span>
</div>
</div>
</div>
{/each}
{/if}
</div>
<div class="mt-4 rounded-lg bg-gray-100 p-4">
<p class="text-sm text-gray-600">MQTT Status: {mqttMessage}</p>
</div>
{/if}
</div>
{:else}
<!-- Fallback to canvas drawing -->
<div class="flex flex-col items-center">
<span class="mb-4">{mqttMessage}</span>
<canvas bind:this={canvasEl} class="border" width="800" height="600"></canvas>
</div>
{/if}
</div> </div>

View file

@ -4,7 +4,24 @@ import { fail } from "@sveltejs/kit";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
export const load = async (event) => { export const load = async (event) => {
return {}; // Fetch all available floors
const floors = await db
.select({ floor: table.floors.floor })
.from(table.floors)
.orderBy(table.floors.floor);
// Provide mock ESP8266 devices for now
const devices = [
{ id: "esp8266-001", name: "ESP8266 #001", type: "esp8266", status: "online" },
{ id: "esp8266-002", name: "ESP8266 #002", type: "esp8266", status: "online" },
{ id: "esp8266-003", name: "ESP8266 #003", type: "esp8266", status: "offline" },
{ id: "esp8266-004", name: "ESP8266 #004", type: "esp8266", status: "online" },
];
return {
floors: floors.map((f) => f.floor),
devices: devices,
};
}; };
export const actions = { export const actions = {
@ -41,4 +58,49 @@ export const actions = {
await db.delete(table.floors).where(eq(table.floors.floor, n)); await db.delete(table.floors).where(eq(table.floors.floor, n));
}, },
savefloor: async (event) => {
const formData = await event.request.formData();
const floorNumber = formData.get("floorNumber");
const floorPlanImage = formData.get("floorPlanImage");
const devices = formData.get("devices");
const n = Number(floorNumber);
if (isNaN(n)) return fail(400, { message: "Invalid floor number!" });
if (!floorPlanImage || !devices) {
return fail(400, { message: "Missing floor plan or device configuration!" });
}
try {
const deviceData = JSON.parse(devices);
// Check if floor exists
const exists = await db
.select({ floor: table.floors.floor })
.from(table.floors)
.where(eq(table.floors.floor, n));
if (exists.length === 0) {
return fail(400, { message: `Floor ${n} does not exist! Please create it first.` });
}
// Update floor with configuration
// Note: In a real implementation, you would store this data properly
// For now, we'll just update the url field as a JSON string
const floorConfig = {
image: floorPlanImage,
devices: deviceData,
};
await db
.update(table.floors)
.set({ url: JSON.stringify(floorConfig) })
.where(eq(table.floors.floor, n));
return { success: true, message: `Floor ${n} configuration saved successfully!` };
} catch (error) {
console.error("Error saving floor configuration:", error);
return fail(500, { message: "Failed to save floor configuration!" });
}
},
}; };

View file

@ -10,19 +10,29 @@
import CardTitle from "$lib/components/ui/card/card-title.svelte"; import CardTitle from "$lib/components/ui/card/card-title.svelte";
import { Thermometer, Droplets, Gauge, Mountain, Upload, Cpu } from "@lucide/svelte"; import { Thermometer, Droplets, Gauge, Mountain, Upload, Cpu } from "@lucide/svelte";
const { form } = $props(); const { form, data } = $props();
let floorPlanImage = $state(null); let floorPlanImage = $state(null);
let availableDevices = $state([ let availableDevices = $state(data.devices || []);
{ id: "esp8266-1", name: "ESP8266 #1", type: "esp8266", status: "online" },
{ id: "esp8266-2", name: "ESP8266 #2", type: "esp8266", status: "online" },
{ id: "esp8266-3", name: "ESP8266 #3", type: "esp8266", status: "offline" },
{ id: "esp8266-4", name: "ESP8266 #4", type: "esp8266", status: "online" },
]);
let placedDevices = $state([]); let placedDevices = $state([]);
let draggedDevice = $state(null); let draggedDevice = $state(null);
let floorPlanRef = $state(null); let floorPlanRef = $state(null);
let selectedFloorNumber = $state("");
let saveMessage = $state(null);
$effect(() => {
if (data.floors && data.floors.length > 0 && !selectedFloorNumber) {
selectedFloorNumber = data.floors[0].toString();
}
});
$effect(() => {
// Update devices when data changes
if (data.devices) {
availableDevices = data.devices;
}
});
function handleFileUpload(event) { function handleFileUpload(event) {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
@ -168,6 +178,65 @@
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600">
Drag ESP8266 devices from below onto the floor plan to place them in specific rooms. Drag ESP8266 devices from below onto the floor plan to place them in specific rooms.
</p> </p>
{#if floorPlanImage && placedDevices.length > 0}
<div class="mt-4 rounded-lg border bg-blue-50 p-4">
<h4 class="mb-3 font-semibold">Save Floor Configuration</h4>
<form
method="post"
action="?/savefloor"
use:enhance={() => {
return async ({ result }) => {
if (result.type === "success") {
saveMessage = { type: "success", text: result.data.message };
// Clear form after successful save
selectedFloorNumber = "";
} else if (result.type === "failure") {
saveMessage = {
type: "error",
text: result.data?.message || "Failed to save configuration",
};
}
};
}}
>
<input type="hidden" name="floorPlanImage" value={floorPlanImage} />
<input type="hidden" name="devices" value={JSON.stringify(placedDevices)} />
<div class="mb-3 flex items-end gap-4">
<div class="flex-1">
<Label for="floorNumber">Select Floor</Label>
<select
name="floorNumber"
id="floorNumber"
bind:value={selectedFloorNumber}
class="mt-1 w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
required
>
{#if data.floors && data.floors.length > 0}
{#each data.floors as floor}
<option value={floor.toString()}>
Floor {floor}
</option>
{/each}
{/if}
</select>
</div>
<Button type="submit" variant="default" disabled={!selectedFloorNumber}>
Save Floor Configuration
</Button>
</div>
</form>
{#if saveMessage}
<p
class="mt-2 text-sm {saveMessage.type === 'success'
? 'text-green-600'
: 'text-red-600'}"
>
{saveMessage.text}
</p>
{/if}
</div>
{/if}
{/if} {/if}
<div class="pt-4"> <div class="pt-4">

View file

@ -18,6 +18,7 @@ function connectMqtt() {
// Replace with your MQTT broker details // Replace with your MQTT broker details
const BROKER_URL = "mqtt://kada49.it:1883"; // Example public broker const BROKER_URL = "mqtt://kada49.it:1883"; // Example public broker
//const BROKER_URL = "mqtt://test.mosquitto.org:1883";
const TOPIC = "esp8266/+/data"; // Replace with your desired topic const TOPIC = "esp8266/+/data"; // Replace with your desired topic
client = mqtt.connect(BROKER_URL); client = mqtt.connect(BROKER_URL);