Skip to content

Commit

Permalink
ping-viewer-next-frontend: Add route for addons/widget/[type]
Browse files Browse the repository at this point in the history
  • Loading branch information
RaulTrombin committed Dec 5, 2024
1 parent ce27b48 commit 6d13000
Show file tree
Hide file tree
Showing 3 changed files with 395 additions and 0 deletions.
277 changes: 277 additions & 0 deletions ping-viewer-next-frontend/src/pages/addons/widget/[type]/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
<template>
<div class="h-screen w-screen bg-transparent" ref="containerRef">

<div v-if="isLoading" class="flex items-center justify-center text-white">
<div class="text-center">
<v-progress-circular indeterminate color="primary" size="64" class="mb-4" />
<div>Connecting to device...</div>
</div>
</div>

<div v-else-if="error" class="h-full w-full flex items-center justify-center">
<div class="text-center p-4 max-w-md text-white">
<v-icon color="error" size="48" class="mb-4">mdi-alert-circle</v-icon>
<h2 class="text-xl mb-2">Error Loading Widget</h2>
<p class="text-gray-400">{{ error }}</p>
<div class="mt-4 text-left text-sm bg-gray-800 p-4 rounded">
<div><strong>type:</strong> {{ route.params.type }}</div>
<div><strong>server:</strong> {{ serverUrl }}</div>
<div><strong>uuid:</strong> {{ deviceId }}</div>
</div>
</div>
</div>

<component v-else-if="widgetComponent && deviceData" :is="widgetComponent" v-bind="widgetProps"
class="h-full w-full bg-transparent" />
</div>
</template>

<script>
import Ping1DLoader from '@components/widgets/sonar1d/Ping1DLoader.vue';
import Ping360Loader from '@components/widgets/sonar360/Ping360Loader.vue';
import { computed, defineComponent, nextTick, onMounted, onUnmounted, ref } from 'vue';
import { useRoute } from 'vue-router';
export default defineComponent({
name: 'WidgetView',
setup() {
const route = useRoute();
const containerRef = ref(null);
const serverUrl = ref('');
const deviceId = ref('');
const error = ref('');
const isLoading = ref(true);
const deviceData = ref(null);
const dimensions = ref({ width: 0, height: 0 });
let resizeObserver = null;
const updateDimensions = () => {
if (!containerRef.value) return;
const rect = containerRef.value.getBoundingClientRect();
dimensions.value = {
width: rect.width,
height: rect.height,
};
};
const widgetType = computed(() => route.params.type?.toLowerCase());
const widgetComponent = computed(() => {
switch (widgetType.value) {
case 'ping360':
return Ping360Loader;
case 'ping1d':
return Ping1DLoader;
default:
return null;
}
});
const websocketUrl = computed(() => {
if (!serverUrl.value || !deviceId.value) return '';
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = new URL(serverUrl.value).host;
return `${wsProtocol}//${host}/ws?device_number=${deviceId.value}`;
});
const commonProps = {
width: window.innerWidth,
height: window.innerHeight,
colorPalette: 'Thermal Blue',
};
const ping360Props = {
...commonProps,
lineColor: '#f44336',
lineWidth: 0.5,
maxDistance: 300,
numMarkers: 5,
showRadiusLines: true,
showMarkers: true,
radiusLineColor: '#4caf50',
markerColor: '#4caf50',
radiusLineWidth: 0.5,
};
const ping1DProps = {
...commonProps,
columnCount: 100,
tickCount: 5,
depthLineColor: '#ffeb3b',
depthTextColor: '#ffeb3b',
currentDepthColor: '#ffeb3b',
confidenceColor: '#4caf50',
textBackground: 'rgba(0, 0, 0, 0.5)',
depthArrowColor: '#f44336',
};
const widgetProps = computed(() => {
if (!deviceData.value) return {};
const baseProps = {
device: deviceData.value,
websocketUrl: websocketUrl.value,
width: dimensions.value.width,
height: dimensions.value.height,
showControls: false,
};
if (widgetType.value === 'ping360') {
return {
...baseProps,
...ping360Props,
width: dimensions.value.width,
height: dimensions.value.height,
};
}
return {
...baseProps,
...ping1DProps,
width: dimensions.value.width,
height: dimensions.value.height,
columnCount: Math.floor(dimensions.value.width / 20),
};
});
onMounted(async () => {
updateDimensions();
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
if (entry.target === containerRef.value) {
updateDimensions();
}
}
});
if (containerRef.value) {
resizeObserver.observe(containerRef.value);
}
window.addEventListener('resize', updateDimensions);
await nextTick();
updateDimensions();
try {
const params = new URLSearchParams(window.location.search);
serverUrl.value = params.get('server') || `${location.protocol}//${location.host}`;
deviceId.value = params.get('uuid') || '';
if (!deviceId.value) {
throw new Error('Missing required parameters: uuid');
}
const requestBody = {
command: 'List',
module: 'DeviceManager',
};
const response = await fetch(`${serverUrl.value}/device_manager/request`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
'Access-Control-Allow-Origin': '*',
},
mode: 'cors',
body: JSON.stringify({
command: 'List',
module: 'DeviceManager',
}),
}).catch((err) => {
return {
ok: true,
json: () =>
Promise.resolve({
DeviceInfo: [
{
id: deviceId.value,
device_type: route.params.type?.toUpperCase() || 'Ping360',
status: 'ContinuousMode',
source: {
UdpStream: {
ip: new URL(serverUrl.value).hostname,
port: new URL(serverUrl.value).port,
},
},
},
],
}),
};
});
if (!response.ok) {
throw new Error(`Failed to connect to server: ${response.status} ${response.statusText}`);
}
const data = await response.json();
let device = data.DeviceInfo?.find((d) => d.id === deviceId.value);
if (!device) {
device = {
id: deviceId.value,
device_type: route.params.type.toUpperCase(),
status: 'ContinuousMode',
source: {
UdpStream: {
ip: new URL(serverUrl.value).hostname,
port: new URL(serverUrl.value).port,
},
},
};
}
if (device.device_type.toLowerCase() !== widgetType.value) {
throw new Error(
`Device type mismatch: expected ${widgetType.value} but got ${device.device_type}`
);
}
deviceData.value = device;
isLoading.value = false;
} catch (err) {
console.error('Widget initialization error:', err);
error.value = err.message;
isLoading.value = false;
}
});
onUnmounted(() => {
if (resizeObserver) {
resizeObserver.disconnect();
}
window.removeEventListener('resize', updateDimensions);
});
return {
containerRef,
error,
isLoading,
deviceData,
widgetComponent,
widgetProps,
route,
serverUrl,
deviceId,
websocketUrl,
};
},
});
</script>
<style scoped>
.h-full {
height: 100%;
}
.w-full {
width: 100%;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
</style>
104 changes: 104 additions & 0 deletions ping-viewer-next-frontend/src/pages/addons/widget/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<template>
<v-container class="py-8">
<v-row>
<v-col cols="12" md="8" class="mx-auto">
<div class="d-flex align-center mb-6">
<v-btn variant="text" :to="{ path: '/' }" class="mr-4">
<v-icon start>mdi-arrow-left</v-icon>
Back to main Application View
</v-btn>
<h1 class="text-h3">Available Widgets</h1>
</div>

<v-card v-for="widget in widgets" :key="widget.type" class="mb-6">
<v-card-title class="text-h5 d-flex align-center">
<v-icon :icon="widget.icon" class="mr-2" />
{{ widget.name }}
</v-card-title>

<v-card-text>
<p class="mb-4">{{ widget.description }}</p>

<v-expansion-panels>
<v-expansion-panel>
<v-expansion-panel-title>Integration Parameters</v-expansion-panel-title>
<v-expansion-panel-text>
<v-list>
<v-list-item v-for="param in widget.parameters" :key="param.name">
<v-list-item-title>
<code>{{ param.name }}</code>
<v-chip size="small" :color="param.required ? 'error' : 'info'"
class="ml-2">
{{ param.required ? 'Required' : 'Optional' }}
</v-chip>
</v-list-item-title>
<v-list-item-subtitle>{{ param.description }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-expansion-panel-text>
</v-expansion-panel>

<v-expansion-panel>
<v-expansion-panel-title>Usage Example</v-expansion-panel-title>
<v-expansion-panel-text>
<v-alert type="info" variant="tonal" class="mb-4">
Replace <code>your-server</code> and <code>device-id</code> with your actual
values.
</v-alert>
<pre class="bg-grey-darken-4 pa-4 rounded"><code>{{ widget.example }}</code></pre>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>

<script setup>
const commonParameters = [
{
name: 'server',
description: 'URL of the PingViewer server (e.g., http://localhost:8080)',
required: false,
},
{
name: 'uuid',
description: 'Device ID of the sensor',
required: true,
},
];
const widgetDefinitions = {
ping1d: {
type: 'ping1d',
name: 'Ping1D Widget',
icon: 'mdi-altimeter',
description: 'Visualize Ping1D sonar data with depth information and waterfall display.',
parameters: [...commonParameters],
example: `<iframe
src="/addons/widget/ping1d?server=http://your-server:8080&device=device-id"
width="800"
height="600"
frameborder="0"
></iframe>`,
},
ping360: {
type: 'ping360',
name: 'Ping360 Widget',
icon: 'mdi-radar',
description: 'Display Ping360 scanning sonar data with real-time visualization.',
parameters: [...commonParameters],
example: `<iframe
src="/addons/widget/ping360?server=http://your-server:8080&device=device-id"
width="800"
height="600"
frameborder="0"
></iframe>`,
},
};
const widgets = Object.values(widgetDefinitions);
</script>
Loading

0 comments on commit 6d13000

Please sign in to comment.