diff --git a/dashboard/src/nav/Navbar.css b/dashboard/src/nav/Navbar.css index 85bd91e..5948aee 100644 --- a/dashboard/src/nav/Navbar.css +++ b/dashboard/src/nav/Navbar.css @@ -6,7 +6,7 @@ flex: 0 0 auto; justify-content: space-between; align-items: center; - padding: 0px 0px; + padding: 0 0; background-color: #0a4777; color: white; height: 60px; @@ -131,25 +131,26 @@ .sidebar-right { position: absolute; - overflow-x: none; top: 0; + right: 0; height: 100%; - width: 300px; + width: 0; background-color: #343a40; - transition: right 0.3s ease; + transition: all 0.3s ease; color: white; - z-index: 1050; - padding: 20px; - right: 0px; - overflow-y: auto; - box-shadow: -2px 0 5px rgba(0,0,0,0.5); -} - -.sidebar-right.hidden { - right: -320px; - box-shadow: none; + z-index: -1; + padding: 10px; + overflow: auto; // hide when width is zero + max-height: 100%; // will not exceed content height + box-shadow: -5px 0 5px rgba(0,0,0,0.5); } .sidebar-right.visible { - right: 0px; + width: 300px; + z-index: 1050; } + +.sidebar-right input { + width: 100px; /* Smaller input width */ + +} \ No newline at end of file diff --git a/dashboard/src/utils/apiCalls.jsx b/dashboard/src/utils/apiCalls.jsx index b048ee8..2a9924a 100644 --- a/dashboard/src/utils/apiCalls.jsx +++ b/dashboard/src/utils/apiCalls.jsx @@ -1,14 +1,14 @@ -export const fitGcps = async (api, widgets, setWidgets, setMessageInfo) => { +export const fitGcps = async (api, widgets, imgDims, epsgCode, setWidgets, setMessageInfo) => { - // temporary test function; - const updateWidgets = () => { - setWidgets((prevWidgets) => - prevWidgets.map((widget) => ({ - ...widget, - fit: {"row": 1075, "col": 1915}, - })) - ); - }; + // // temporary test function; + // const updateWidgets = () => { + // setWidgets((prevWidgets) => + // prevWidgets.map((widget) => ({ + // ...widget, + // fit: {"row": 1075, "col": 1915}, + // })) + // ); + // }; try { // Checks! if (widgets.length < 0) { @@ -18,31 +18,65 @@ export const fitGcps = async (api, widgets, setWidgets, setMessageInfo) => { } // Extract coordinates into separated lists - const src = widgets.map(({ coordinates }) => [ - parseFloat(coordinates.x) || 0, - parseFloat(coordinates.y) || 0, - coordinates.z === '' || isNaN(Number(coordinates.z)) ? NaN : parseFloat(coordinates.z) + const dst = widgets.map(({ coordinates }) => [ + parseFloat(coordinates.x) || null, + parseFloat(coordinates.y) || null, + parseFloat(coordinates.z) || null ]); - const dst = widgets.map(({ coordinates }) => [ - parseFloat(coordinates.row, 10) || 0, - parseFloat(coordinates.col, 10) || 0, + const src = widgets.map(({ coordinates }) => [ + parseFloat(coordinates.col, 10) || null, + parseFloat(coordinates.row, 10) || null, ]); + // check if dst contains null + if (dst.some(row => row.includes(null))) { + setMessageInfo('error', 'GCP real-world coordinates are not complete. Ensure the coordinates are entered correctly and try again.'); + } + + // check if src contains null values + if (src.some(row => row.includes(null))) { + setMessageInfo('error', 'GCPs must have valid row and column coordinates. Please click the GCPs into the image frame to fix this.'); + return; + } const payload = { "src": src, "dst": dst, + "height": imgDims.height, + "width": imgDims.width, + "crs": epsgCode.toString() }; console.log('Sending payload:', payload); - updateWidgets(); - console.log(widgets); -// // Send data to API endpoint -// const response = await api.post('https://api.example.com/endpoint', payload); -// console.log('API Response:', response.data); + // Send data to API endpoint + const response = await api.post('/camera_config/fit_perspective', payload); + // Extract `src_est` and `dst_est` from API response + const { src_est, dst_est, error } = response.data; + // Map the fitted coordinates back to the widgets + setWidgets((prevWidgets) => + prevWidgets.map((widget, index) => { + return { + ...widget, + fit: { + row: src_est ? src_est[index][1] : null, // row from src_est + col: src_est ? src_est[index][0] : null, // col from src_est + x: dst_est ? dst_est[index][0] : null, // x from dst_est + y: dst_est ? dst_est[index][1] : null, // y from dst_est + z: dst_est ? dst_est[index][2] : null, // z from dst_est + } + }; + }) + ); + const err_round = Math.round(error * 1000) / 1000; + if (err_round > 0.1) { + setMessageInfo('warning', `GCPs successfully fitted, but with a large average error: ${err_round} m.`); + return;} + setMessageInfo('success', `GCPs successfully fitted to image, average error: ${err_round} m.`); + // map the estimated points on the widgets for plotting + // updateWidgets(); } catch (error) { - console.error('Failed to send coordinates:', error); + setMessageInfo('error', 'Failed to send coordinates:' + error.response.data.detail); // Optionally, handle errors (e.g., display an error message) } }; diff --git a/dashboard/src/utils/leafletUtils.js b/dashboard/src/utils/leafletUtils.js index 7551e38..7eae759 100644 --- a/dashboard/src/utils/leafletUtils.js +++ b/dashboard/src/utils/leafletUtils.js @@ -1,7 +1,7 @@ import L from "leaflet"; // Import Leaflet for DivIcon // Function to create a custom DivIcon with dynamic RGBA colors -export const createCustomMarker = (color) => { +export const createCustomMarker = (color, id) => { return L.divIcon({ className: "custom-marker", // Base class (can use CSS for additional styles) @@ -13,11 +13,11 @@ export const createCustomMarker = (color) => { top: -0.75rem; transform: rotate(45deg); border-radius: 1.5rem 1.5rem 0; - alignItems: center; + align-items: center; justify-content: center; border: 1px solid rgba(0, 0, 0, 0.5); - color: white - ">
1
`, + color: black + ">
${id}
`, iconSize: [30, 30], // Marker size iconAnchor: [15, 15], // Position the icon properly popupAnchor: [0, -20], // Adjust popup anchor relative to marker diff --git a/dashboard/src/views/calibration.css b/dashboard/src/views/calibration.css index b8021c7..c37ad33 100644 --- a/dashboard/src/views/calibration.css +++ b/dashboard/src/views/calibration.css @@ -4,7 +4,7 @@ flex-direction: column; max-height: 100%; box-sizing: border-box; - margin: 0px; + margin: 0; } .tabbed-layout { @@ -24,7 +24,7 @@ /* Style for individual tabs */ .tabs-row button { - border: 0px solid #ccc; + border: 0 solid #ccc; border-radius: 5px 5px 0 0; /* Rounded top corners */ padding: 10px 20px; margin-right: 5px; /* Space between tabs */ @@ -47,25 +47,33 @@ font-weight: bold; /* Highlight active tab with bold text */ } +.tab-container { + display: flex; /* Enables flexbox layout */ + width: 100%; /* Full width of the container */ + height: 100%; /* Optional, depending on the layout */ + overflow: hidden; /* Ensures no extra content spills outside */ + position: relative; /* Optional for contextual positioning */ +} -/* .tab-content { - flex: 1; - padding: 0px; -} */ - .tab-content { - display: flex; - margin-top: 0px; - margin-bottom: 0px; + flex: 1; /* Fills the remaining space by default */ + overflow: hidden; // to ensure side bar is not visible. + transition: all 0.3 ease; + margin-top: 0; + margin-bottom: 0; position: relative; - height: 100%; - padding: 0px; + height: 80vh; + padding: 0; background: #fff; /*border: 1px solid #ccc; /*border-radius: 0 5px 5px 5px; /* Rounded bottom corners (Excel style) */ } +.tab-content.shifting { + margin-right: 300px; +} + .img-calibration { width: 100%; height: auto; diff --git a/dashboard/src/views/calibration.jsx b/dashboard/src/views/calibration.jsx index 1add48d..908e4d5 100644 --- a/dashboard/src/views/calibration.jsx +++ b/dashboard/src/views/calibration.jsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; -import { FaChevronLeft, FaChevronRight } from 'react-icons/fa'; -import { useMessage } from '../messageContext'; +import { useState } from 'react'; +import {FaChevronLeft, FaChevronRight} from 'react-icons/fa'; +import {useMessage} from '../messageContext'; import MessageBox from '../messageBox'; import VideoTab from './calibrationTabs/videoTab' import XYZWidget from './calibrationTabs/XyzWidget'; @@ -9,8 +9,8 @@ import api from '../api'; import '../nav/Navbar.css' import './calibration.css'; // Ensure the styles reflect the updated layout. -import { createCustomMarker } from '../utils/leafletUtils'; -import { fitGcps } from '../utils/apiCalls'; +import {createCustomMarker} from '../utils/leafletUtils'; +import {fitGcps} from '../utils/apiCalls'; const Calibration = () => { @@ -19,10 +19,10 @@ const Calibration = () => { const [widgets, setWidgets] = useState([]); const [selectedWidgetId, setSelectedWidgetId] = useState(null); // To track which widget is being updated const [nextId, setNextId] = useState(1); // widget ids increment automatically - const [dots, setDots] = useState([]); // Array of { x, y, id } objects + const [dots, setDots] = useState({}); // Array of { x, y, id } objects const [GCPsVisible, setGCPsVisible] = useState(false); // State to toggle GCP menu right-side const [epsgCode, setEpsgCode] = useState(4326); - const mapIsVisible = activeTab === "map"; // Check Map Tab visibility + const [imgDims, setImgDims] = useState(null); const [formData, setFormData] = useState({ video: '', controlPoints: '', @@ -30,10 +30,10 @@ const Calibration = () => { }); // allow for setting messages - const { setMessageInfo } = useMessage(); + const {setMessageInfo} = useMessage(); // make some colors - const rainbowColors = Array.from({ length: 10 }, (_, i) => { + const rainbowColors = Array.from({length: 10}, (_, i) => { const hue = (i / 10) * 360; // Distributes hues evenly across 360 degrees return `hsl(${hue}, 100%, 50%)`; }); @@ -49,30 +49,32 @@ const Calibration = () => { })); }; - const handleSubmit = (e) => { - e.preventDefault(); - console.log('Form submitted:', formData); - }; + // TODO implement a submit button for storing CameraConfig in database + // const handleSubmit = (e) => { + // e.preventDefault(); + // console.log('Form submitted:', formData); + // }; const addWidget = () => { setWidgets((prevWidgets) => { - const color = rainbowColors[(nextId -1) % rainbowColors.length]; + const color = rainbowColors[(nextId - 1) % rainbowColors.length]; return [ ...prevWidgets, { id: nextId, color: color, - coordinates: { x: '', y: '', z: '', row: '', col: '' }, - icon: createCustomMarker(color) + coordinates: {x: '', y: '', z: '', row: '', col: ''}, + icon: createCustomMarker(color, nextId) }, - ]}); + ] + }); setNextId((prevId) => prevId + 1); // increment the unique id for the next widget }; const updateWidget = (id, updatedCoordinates) => { setWidgets((prevWidgets) => prevWidgets.map((widget) => - widget.id === id ? { ...widget, coordinates: updatedCoordinates } : widget + widget.id === id ? {...widget, coordinates: updatedCoordinates} : widget ) ); }; @@ -82,7 +84,7 @@ const Calibration = () => { // also delete the dot setDots((prevDots) => { // Copy the previous state object - const newDots = { ...prevDots }; + const newDots = {...prevDots}; delete newDots[id]; return newDots; }); @@ -98,12 +100,6 @@ const Calibration = () => { const toggleMenu = () => { setGCPsVisible((prev) => !prev); }; - // Detects clicks outside the menu and closes it - const handleBackgroundClick = (e) => { - if (menuVisible) { - setMenuVisible(false); - } - }; // Function to handle file upload and processing const handleFileUpload = (event) => { @@ -151,14 +147,15 @@ const Calibration = () => { return { color: color, id: index + 1, // Unique ID for widget - coordinates: { x, y, z, row: "", col: "" }, - icon: createCustomMarker(color) + coordinates: {x, y, z, row: "", col: ""}, + icon: createCustomMarker(color, index + 1) }; - }); + }); setWidgets(newWidgets); + setNextId(newWidgets.length + 1); // ensure the next ID is ready for a new XYZ widget setFileError(null); // Clear error state - } catch (err) { + } catch { setFileError("Failed to parse GeoJSON: Invalid file format."); } }; @@ -171,54 +168,54 @@ const Calibration = () => { }; - return (
- - {GCPsVisible &&
} +

Camera calibration

- {/* Tabs row */} -
- - - -
- + {/* Tabs row */} +
+ + + +
+
{/* Tab content */} -
+
{activeTab === 'video' && ( - + )} {activeTab === 'threed' && (
@@ -231,100 +228,103 @@ const Calibration = () => {
)} {activeTab === 'map' && - ( - - )} -
- {/* Right-side button to toggle the menu */} - - {/* Sliding menu (right tabs column) */} -
- - ORC Logo - {' '} GCPs - -
-
- {/* File Upload Button */} - - - {/* Error Message */} - {fileError &&
{fileError}
} - - {/* CRS Input */} -
- - setEpsgCode(e.target.value)} - placeholder="4326" - style={{ width: '300px' }} - /> + ( + + )}
- - - {widgets.map((widget) => ( -
- setSelectedWidgetId(widget.id) - } - style={{ - border: selectedWidgetId === widget.id ? `4px solid ${widget.color}` : `1px solid ${widget.color}`, - marginTop: '10px', - marginBottom: '10px', - padding: '5px', - color: 'white', - cursor: 'pointer', - }} - > - updateWidget(id, coordinates)} - onDelete={deleteWidget} + {/* Right-side button to toggle the menu */} + + + {/* Sliding menu (right tabs column) */} +
+ + ORC Logo + {' '} GCPs + +
+
+ {/* File Upload Button */} + + + {/* Error Message */} + {fileError &&
{fileError}
} + + {/* CRS Input */} +
+ + setEpsgCode(e.target.value)} + placeholder="4326" + style={{width: '200px'}} />
- ))} -
+ + + {widgets.map((widget) => ( +
+ setSelectedWidgetId(widget.id) + } + style={{ + border: selectedWidgetId === widget.id ? `4px solid ${widget.color}` : `1px solid ${widget.color}`, + marginTop: '10px', + marginBottom: '10px', + padding: '5px', + color: 'white', + cursor: 'pointer', + }} + > + updateWidget(id, coordinates)} + onDelete={deleteWidget} + /> +
+ ))} +
+
- {/* Submit button section */} -
- -
+ {/*/!* Submit button section *!/*/} + {/*
*/} + {/* */} + {/*
*/}
); }; diff --git a/dashboard/src/views/calibrationTabs/mapTab.jsx b/dashboard/src/views/calibrationTabs/mapTab.jsx index 382df1b..92c94d5 100644 --- a/dashboard/src/views/calibrationTabs/mapTab.jsx +++ b/dashboard/src/views/calibrationTabs/mapTab.jsx @@ -1,10 +1,10 @@ -import React, { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect } from "react"; import proj4 from 'proj4'; -import epsg from 'epsg-index'; -import epsgDB from '../../proj-db.json'; // Adjust the path as needed import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet"; -import { createCustomMarker } from '../../utils/leafletUtils'; +import L from 'leaflet'; + import "leaflet/dist/leaflet.css"; +import epsgDB from '../../proj-db.json'; // Adjust the path as needed // Ensure proj4 is set up to convert between CRS definitions @@ -14,10 +14,10 @@ proj4.defs('EPSG:4326', '+proj=longlat +datum=WGS84 +no_defs'); console.log(proj4.defs['EPSG:4326']); // Output: "+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs" console.log(proj4.defs[epsgDB[28992]]); // Output: "+proj=utm +zone=33 +datum=WGS84 +units=m +no_defs" -const MapTab = ({epsgCode, widgets, selectedWidgetId, updateWidget, mapIsVisible}) => { +import PropTypes from "prop-types"; + +const MapTab = ({epsgCode, widgets, mapIsVisible}) => { const [mapLayers, setMapLayers] = useState("OSM"); // Map layer type - const [controlPoints, setControlPoints] = useState([]); // Coordinate set - const [coordinateForms, setCoordinateForms] = useState([]); // Track widgets const mapRef = useRef(null); // Define the default CRS as WGS84 const defaultCrs = 'EPSG:4326'; @@ -78,12 +78,12 @@ const MapTab = ({epsgCode, widgets, selectedWidgetId, updateWidget, mapIsVisible // try to get the proj4 from the defs directly try { proj4Def = proj4.defs[`EPSG:${sourceCrs.toString()}`] - } catch (error) { - console.error(`Projection ${sourceCode} not found in proj4 lib`) + } catch { + console.error(`Projection ${sourceCrs} not found in proj4 lib`) } } - } catch (error) { - console.error(`Projection ${sourceCode} not found in proj4 or EPSG database`); + } catch { + console.error(`Projection ${sourceCrs} not found in proj4 or EPSG database`); return null; } } else { @@ -100,24 +100,25 @@ const MapTab = ({epsgCode, widgets, selectedWidgetId, updateWidget, mapIsVisible }; /* first div is problematic, makes the page overflow */ return ( -
+
{/* Map container */} {/* Tile layers */} {mapLayers === "OSM" && ( )} {mapLayers === "Satellite" && ( - + + )} {/* Render dynamically added markers */} @@ -144,8 +145,8 @@ const MapTab = ({epsgCode, widgets, selectedWidgetId, updateWidget, mapIsVisible
{ +import {useState, useEffect} from 'react'; +import {TransformComponent, useTransformEffect, useTransformInit} from 'react-zoom-pan-pinch'; +import './photoComponent.css'; +import PropTypes from 'prop-types'; + +const PhotoComponent = ({ + imageRef, + selectedWidgetId, + updateWidget, + widgets, + scale, + dots, + imgDims, + setDots, + setImgDims + }) => { const [transformState, setTransformState] = useState(null); // state of zoom is stored here - const [photoBbox, setPhotoBbox ] = useState(null); - const [imgDims, setImgDims] = useState(null); + const [photoBbox, setPhotoBbox] = useState(null); const [fittedPoints, setFittedPoints] = useState([]); - useTransformInit(({state, instance}) => { + const updateFittedPoints = () => { + const fP = widgets.map(({id, fit, color}) => { + if (!fit) return null; // Skip widgets without the `fit` property + const {row, col} = fit; + const screenPoint = convertToPhotoCoordinates(row, col); + return { + "id": id, + "x": screenPoint.x, + "y": screenPoint.y, + "color": color + } + + }); + setFittedPoints(fP); + } + + // update the dot locations when user resizes the browser window + const updateDots = () => { + try { + const updatedDots = Object.entries(dots).reduce((newDots, [id, dot]) => { + const newX = dot.xNorm * photoBbox.width / transformState.scale; // + const newY = dot.yNorm * photoBbox.height / transformState.scale; // + // Recalculate the actual position relative to the new photoBbox and dimensions + newDots[id] = { + ...dot, + x: newX, + y: newY, + }; + return newDots; + }, {}); + setDots(updatedDots); + } catch { + console.log("Skipping dot rendering, image not yet initialized") + } + + } + + // run these as soon as the TransformComponent is ready + useTransformInit(({state}) => { // ensure the zoom/pan state is stored in a react state at the mounting of the photo element setTransformState(state); + // ensure we have a method (event listener) to reproject the plotted points upon changes in window size + const handleResize = () => { + const imgElement = imageRef.current; + setPhotoBbox(imgElement.getBoundingClientRect()); + setTransformState(state); // Update the transformState on every transformation + }; + + window.addEventListener("resize", handleResize); + handleResize(); + + return () => { + window.removeEventListener("resize", handleResize); + }; + + }, [imageRef, updateFittedPoints]); + + // triggered when user resizes the window, after this, the dot locations must be updated + useEffect(() => { + updateDots(); + }, [photoBbox]); // TODO: updateDots is a dependency, but it changes all the time, perhaps put updateDots inside useEffect - }, []); useEffect(() => { try { @@ -19,11 +87,15 @@ const PhotoComponent = ({imageRef, selectedWidgetId, updateWidget, widgets, scal setImgDims({width: imageRef.current.naturalWidth, height: imgElement.naturalHeight}); setPhotoBbox(imgElement.getBoundingClientRect()); updateFittedPoints(); + + // updateFittedPoints(); } catch { - console.log("Image not yet initialized")} - }, [widgets, transformState]); + console.error("Image not yet initialized") + } + }, [widgets, transformState, window]); - useTransformEffect(({ state, instance }) => { + + useTransformEffect(({state}) => { const imgElement = imageRef.current; setPhotoBbox(imgElement.getBoundingClientRect()); setTransformState(state); // Update the transformState on every transformation @@ -38,26 +110,10 @@ const PhotoComponent = ({imageRef, selectedWidgetId, updateWidget, widgets, scal // Function to convert row/column to pixel coordinates const convertToPhotoCoordinates = (row, col) => { const x = col / imgDims.width * photoBbox.width / transformState.scale; - const y = row / imgDims.height * photoBbox.height / transformState.Scale; - return { x, y }; + const y = row / imgDims.height * photoBbox.height / transformState.scale; + return {x, y}; }; - const updateFittedPoints = () => { - const fP = widgets.map(({ id, fit, color }) => { - if (!fit) return null; // Skip widgets without the `fit` property - const { row, col } = fit; - const screenPoint = convertToPhotoCoordinates(row, col); - return { - "id": id, - "x": screenPoint.x, - "y": screenPoint.y, - "color": color - } - - }); - setFittedPoints(fP); - console.log(fP); - } const handlePhotoClick = (event) => { event.stopPropagation(); if (!transformState) { @@ -70,7 +126,7 @@ const PhotoComponent = ({imageRef, selectedWidgetId, updateWidget, widgets, scal const clickY = event.clientY - photoBbox.top; // Account for current zoom and pan state - const { previousScale, scale, positionX, positionY } = transformState; + const {scale} = transformState; // Use normalized coordinates relative to the original image dimensions const normalizedX = clickX / photoBbox.width; // X in percentage of displayed width @@ -79,93 +135,105 @@ const PhotoComponent = ({imageRef, selectedWidgetId, updateWidget, widgets, scal // Adjust for zoom scale using zoom state const adjustedX = clickX / scale; const adjustedY = clickY / scale; + + // Calculate the row and column on the **original image** (as percentages) const originalRow = Math.round(normalizedY * imgDims.height * 100) / 100; const originalCol = Math.round(normalizedX * imgDims.width * 100) / 100; - // Add the new dot to the state with the ID of the associated widget - if (!selectedWidgetId) { - alert("Please select a widget to update its row/column."); - return; - } - - // Update the dots - setDots((prevDots) => ({ - ...prevDots, - [selectedWidgetId]: { x: adjustedX, y: adjustedY, scale: scale, color: getWidgetById(selectedWidgetId).color }, - })); + // Add the new dot to the state with the ID of the associated widget + if (!selectedWidgetId) { + alert("Please select a widget to update its row/column."); + return; + } - updateWidget(selectedWidgetId, { - ...widgets.find((widget) => widget.id === selectedWidgetId).coordinates, - row: originalRow, - col: originalCol, - }); + // Update the dots + setDots((prevDots) => ({ + ...prevDots, + [selectedWidgetId]: { + x: adjustedX, + y: adjustedY, + xNorm: normalizedX, + yNorm: normalizedY, + scale: scale, + color: getWidgetById(selectedWidgetId).color + }, + })); + + updateWidget(selectedWidgetId, { + ...widgets.find((widget) => widget.id === selectedWidgetId).coordinates, + row: originalRow, + col: originalCol, + }); } + PhotoComponent.propTypes = { + imageRef: PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + selectedWidgetId: PropTypes.number, + updateWidget: PropTypes.func.isRequired, + widgets: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.number.isRequired, + fit: PropTypes.shape({ + row: PropTypes.number, + col: PropTypes.number + }), + color: PropTypes.string + })).isRequired, + scale: PropTypes.number, + dots: PropTypes.object.isRequired, + imgDims: PropTypes.shape({ + width: PropTypes.number.isRequired, + height: PropTypes.number.isRequired + }), + setDots: PropTypes.func.isRequired, + setImgDims: PropTypes.func.isRequired + }; + return ( - - img-calibration + + img-calibration {/* Render colored dots */} {Object.entries(dots).map(([widgetId, dot]) => { - const imgElement = imageRef.current; return ( -
- {widgetId} + {widgetId}
); - })} - {/* Render colored dots */} + })} + {/* Render fitted points */} {fittedPoints.map((point) => { if (!point || point.x === undefined || point.y === undefined) return null; - return ( -
- {point.id} + +
); - })} + })}
); diff --git a/dashboard/src/views/calibrationTabs/videoTab.jsx b/dashboard/src/views/calibrationTabs/videoTab.jsx index c943fc8..29f6246 100644 --- a/dashboard/src/views/calibrationTabs/videoTab.jsx +++ b/dashboard/src/views/calibrationTabs/videoTab.jsx @@ -1,8 +1,10 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { TransformWrapper, TransformComponent, useTransformEffect, useTransformInit } from 'react-zoom-pan-pinch'; +import { useState, useRef } from 'react'; +import { TransformWrapper } from 'react-zoom-pan-pinch'; import PhotoComponent from './photoComponent'; -const VideoTab = ({widgets, selectedWidgetId, updateWidget, dots, setDots}) => { +import PropTypes from 'prop-types'; + +const VideoTab = ({widgets, selectedWidgetId, updateWidget, dots, imgDims, setDots, setImgDims}) => { const [scale, setScale] = useState(1); const imageRef = useRef(null); // Reference to image within TransFormWrapper @@ -28,20 +30,30 @@ const VideoTab = ({widgets, selectedWidgetId, updateWidget, dots, setDots}) => { widgets={widgets} scale={scale} dots={dots} + imgDims={imgDims} setDots={setDots} + setImgDims={setImgDims} />
Click on the photo to select row/column
-
-
-

Current Coordinates:

-
{JSON.stringify(widgets, null, 2)}
+{/*

Current Coordinates:

*/} +{/*
{JSON.stringify(widgets, null, 2)}
*/}
); }; +VideoTab.propTypes = { + widgets: PropTypes.array.isRequired, + selectedWidgetId: PropTypes.oneOfType([PropTypes.number]), + updateWidget: PropTypes.func.isRequired, + dots: PropTypes.object.isRequired, + imgDims: PropTypes.object, + setDots: PropTypes.func.isRequired, + setImgDims: PropTypes.func.isRequired, +}; + export default VideoTab; diff --git a/dashboard/src/views/home.jsx b/dashboard/src/views/home.jsx index c3ab9e0..76144ea 100644 --- a/dashboard/src/views/home.jsx +++ b/dashboard/src/views/home.jsx @@ -1,8 +1,7 @@ -import React, {useState, useEffect} from 'react'; +import { useState } from 'react'; import { FaTimes } from 'react-icons/fa'; import reactLogo from '/react.svg' import orcLogo from '/orc_favicon.svg' -import api from "../api" const Home = () => { const [count, setCount] = useState(0) diff --git a/tests/test_routers/test_camera_config.py b/tests/test_routers/test_camera_config.py index 932c180..b980974 100644 --- a/tests/test_routers/test_camera_config.py +++ b/tests/test_routers/test_camera_config.py @@ -24,18 +24,44 @@ def gcps_data(): [192105.2958125107, 313172.0257530752, 150.616], [192110.35620407888, 313162.5371485311, 150.758], ], - "crs": None, + "crs": "28992", "height": 1080, "width": 1920 } +@pytest.fixture +def gcps_data_lat_lon(): + return { + "src": [[158, 314], [418, 245], [655, 162], [948, 98], [1587, 321],[1465, 747]], + "dst": [ + [5.913539646666668, 50.80710705166666, 150.831], + [5.913524096666666, 50.80712979166666, 150.717], + [5.913501468333333, 50.80715845, 150.807], + [5.913461251666667, 50.80721228666666, 150.621], + [5.913580743333333, 50.80723664166667, 150.616], + [5.91365156, 50.80715102666666, 150.758], + ], + "crs": "4326", + "height": 1080, + "width": 1920 + + } + + def test_fit_perspective_success(gcps_data): response = client.post("/camera_config/fit_perspective", json=gcps_data) assert response.status_code == 200 assert "src_est" in response.json() assert "dst_est" in response.json() +def test_fit_perspective_lat_lon_success(gcps_data_lat_lon): + response = client.post("/camera_config/fit_perspective", json=gcps_data_lat_lon) + assert response.status_code == 200 + assert "src_est" in response.json() + assert "dst_est" in response.json() + + def test_fit_perspective_mismatched_input_lengths(gcps_data): # reduce nr of points gcps_data["src"] = gcps_data["src"][:-1]