add floor plan
This commit is contained in:
parent
5935064606
commit
5d689e6005
5 changed files with 254 additions and 41 deletions
|
@ -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
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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!" });
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue