Skip to content

Commit

Permalink
feat: add location for measurements (#377)
Browse files Browse the repository at this point in the history
* upgrade drizzle and drizzle-kit

* add location to measurements for mobile devices

* drop last migration

* add locations for mobile devices

* workaround to handle postgis geometry in query

* remove migrations

* add locations

* disable esLint for now

* get rid of `button can not be a descendant of button` error

* make tags optional

* mobile box overview

* add dynamic popup to mobile trajectories

* enhance mobile boxes

* add build target

* fix `top level await` error

* add trips switch

* remove colors when trips are deactivated

* allow to select two sensors on mobile device

* enable switching overlaying mobile sensor

* add sensor unit to dependencies in MobileBoxLayer effect

* refactor(routes): update route configuration and dependencies

* refactor(donut-chart-cluster): simplify destructuring and formatting in component

* refactor(device): comment out time column in getDevice function

* refactor(graph): rename LineWithZoom to GraphWithZoom and update zoom state management

* refactor(mobile): update MobileBoxView structure and clean up MobileOverviewLayer formatting

* refactor(device): add time column to extras in getDevice function

* refactor(package): remove chartjs-scale-timestack dependency from package.json

* update package-lock.json

* refactor(color): update color palette and introduce dynamic color calculation

* refactor(device-detail): improve image handling and update log entry styles

* delete relation to location on device deletion

---------

Co-authored-by: freds-dev <[email protected]>
  • Loading branch information
mpfeil and freds-dev authored Dec 18, 2024
1 parent 9b7aa64 commit 4b18bc2
Show file tree
Hide file tree
Showing 33 changed files with 5,988 additions and 999 deletions.
5 changes: 2 additions & 3 deletions app/components/aggregation-filter.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Filter } from "lucide-react";
import { Button } from "./ui/button";
import { Separator } from "./ui/separator";
import { Badge } from "./ui/badge";
import { useSearchParams, useSubmit } from "@remix-run/react";
Expand Down Expand Up @@ -61,7 +60,7 @@ export function AggregationFilter() {
"flex h-10 w-full items-center justify-between bg-transparent px-3 py-2 text-sm placeholder:text-slate-500 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-800 dark:placeholder:text-slate-400"
}
>
<Button variant="outline" size="sm" className="h-8">
<div className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-slate-950 dark:focus-visible:ring-slate-300 h-8 border border-slate-200 bg-white hover:bg-slate-100 hover:text-slate-900 dark:border-slate-800 dark:bg-slate-950 dark:hover:bg-slate-800 dark:hover:text-slate-50 ">
<Filter className="mr-2 h-4 w-4" />
Aggregation
<>
Expand All @@ -70,7 +69,7 @@ export function AggregationFilter() {
{selectedAggregation?.label}
</Badge>
</>
</Button>
</div>
</SelectPrimitive.Trigger>
<SelectContent>
{aggregations.map((aggregation) => (
Expand Down
31 changes: 14 additions & 17 deletions app/components/device-detail/device-detail-box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
CalendarPlus,
Hash,
LandPlot,
Image as ImageIcon,
} from "lucide-react";
import { Fragment, useEffect, useRef, useState } from "react";
import type { DraggableData } from "react-draggable";
Expand All @@ -53,7 +54,7 @@ import { getArchiveLink } from "~/utils/device";
import { useBetween } from "use-between";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
import { isTablet, isBrowser } from "react-device-detect";
import type { Device, Sensor, SensorWithMeasurement } from "~/schema";
import type { SensorWithMeasurement } from "~/schema";
import { format, formatDistanceToNow } from "date-fns";
import {
Card,
Expand Down Expand Up @@ -85,11 +86,6 @@ export interface MeasurementProps {
max_value: string;
}

export interface DeviceAndSelectedSensors {
device: Device;
selectedSensors: Sensor[];
}

const useCompareMode = () => {
const [compareMode, setCompareMode] = useState(false);
return { compareMode, setCompareMode };
Expand Down Expand Up @@ -204,11 +200,6 @@ export default function DeviceDetailBox() {
return () => clearInterval(interval);
}, [refreshOn, refreshSecond]);

const getDeviceImage = (imageUri: string) =>
imageUri !== null
? `https://opensensemap.org/userimages/${imageUri}`
: "https://images.placeholders.dev/?width=400&height=350&text=No%20image&bgColor=%234fae48&textColor=%23727373";

return (
<>
{open && (
Expand Down Expand Up @@ -331,11 +322,17 @@ export default function DeviceDetailBox() {
<div className="no-scrollbar relative flex-1 overflow-y-scroll">
<div className="space-y-4 sm:space-y-0 sm:flex sm:space-x-4">
<div className="md:w-1/2">
<img
className="w-full object-cover rounded-lg"
alt="device_image"
src={getDeviceImage(data.device.image)}
></img>
{data.device.image ? (
<img
className="w-full object-cover rounded-lg"
alt="device_image"
src={data.device.image}
></img>
) : (
<div className="w-full object-cover rounded-lg text-muted-foreground">
<ImageIcon strokeWidth={1} className="w-full h-full" />
</div>
)}
</div>
<div className="sm:w-1/2 space-y-2">
<InfoItem
Expand All @@ -362,7 +359,7 @@ export default function DeviceDetailBox() {
/>
</div>
</div>
{data.device.tags.length > 0 && (
{data.device.tags?.length > 0 && (
<div className="pt-4">
<div className="space-y-2">
<div className="text-sm font-medium text-muted-foreground">
Expand Down
4 changes: 2 additions & 2 deletions app/components/device-detail/entry-logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export default function EntryLogs({
<p className="font-bold pb-4">Logs</p>
<div className="flex items-center">
<div className="flex items-start space-x-4 w-full">
<div className="shrink-0 w-10 h-10 rounded-full bg-primary flex items-center justify-center">
<Activity className="text-primary-foreground w-5 h-5" />
<div className="shrink-0 w-10 h-10 rounded-full border-4 border-muted-foreground text-muted-foreground flex items-center justify-center">
<Activity className="w-5 h-5" />
</div>
<div className="flex-grow">
<p className="text-sm font-medium mb-2">{entryLogs[0].content}</p>
Expand Down
121 changes: 84 additions & 37 deletions app/components/device-detail/graph.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
useNavigate,
useNavigation,
useSearchParams,
} from "@remix-run/react";
import { useNavigate, useNavigation, useSearchParams } from "@remix-run/react";
import {
Chart as ChartJS,
LineElement,
Expand All @@ -18,7 +14,7 @@ import "chartjs-adapter-date-fns";
import { Line } from "react-chartjs-2";
import type { ChartOptions } from "chart.js";
// import { de, enGB } from "date-fns/locale";
import { useMemo, useRef, useState, useEffect } from "react";
import { useMemo, useRef, useState, useEffect, useContext } from "react";
import { Download, RefreshCcw, X } from "lucide-react";
import type { DraggableData } from "react-draggable";
import Draggable from "react-draggable";
Expand All @@ -42,6 +38,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "../ui/tooltip";
import { HoveredPointContext } from "../map/layers/mobile/mobile-box-layer";

ChartJS.register(
LineElement,
Expand All @@ -55,7 +52,7 @@ ChartJS.register(
);

// ClientOnly component to handle the plugin that needs window
const LineWithZoom = (props: any) => {
const GraphWithZoom = (props: any) => {
useMemo(() => {
// Dynamically import the zoom plugin
import("chartjs-plugin-zoom").then(({ default: zoomPlugin }) => {
Expand Down Expand Up @@ -85,11 +82,15 @@ export default function Graph({
startDate,
endDate,
}: GraphProps) {
const { setHoveredPoint } = useContext(HoveredPointContext);
const navigation = useNavigation();
const navigate = useNavigate();
const [offsetPositionX, setOffsetPositionX] = useState(0);
const [offsetPositionY, setOffsetPositionY] = useState(0);
const [isZoomed, setIsZoomed] = useState(false); // State to track zoom
const [currentZoom, setCurrentZoom] = useState<{
xMin: number;
xMax: number;
} | null>(null); // To track zoom
const [searchParams, setSearchParams] = useSearchParams();
const [colorPickerState, setColorPickerState] = useState({
open: false,
Expand All @@ -99,7 +100,24 @@ export default function Graph({
const isAggregated = aggregation !== "raw";

const nodeRef = useRef(null);
const chartRef = useRef<ChartJS<"line">>(null); // Define chartRef here
const chartRef = useRef<ChartJS<"line">>(null);

useEffect(() => {
if (chartRef.current) {
const canvas = chartRef.current.canvas;

const handleMouseLeave = () => {
setHoveredPoint(null); // Clear the hovered point when the mouse leaves the chart area
};

canvas.addEventListener("mouseleave", handleMouseLeave);

// Cleanup
return () => {
canvas.removeEventListener("mouseleave", handleMouseLeave);
};
}
}, [chartRef, setHoveredPoint]);

// get theme from tailwind
const [theme] = useTheme();
Expand Down Expand Up @@ -127,6 +145,7 @@ export default function Graph({
data: sensor.data.map((measurement) => ({
x: measurement.time,
y: measurement.value,
locationId: measurement.locationId,
})),
pointRadius: 0,
borderColor: sensor.color,
Expand All @@ -143,6 +162,7 @@ export default function Graph({
data: sensor.data.map((measurement) => ({
x: measurement.time,
y: measurement.min_value,
locationId: null,
})),
borderColor: sensor.color + "33",
backgroundColor: sensor.color + "33",
Expand All @@ -155,6 +175,7 @@ export default function Graph({
data: sensor.data.map((measurement) => ({
x: measurement.time,
y: measurement.max_value,
locationId: null,
})),
borderColor: sensor.color + "33",
backgroundColor: sensor.color + "33",
Expand Down Expand Up @@ -194,6 +215,7 @@ export default function Graph({
data: sensor.data.map((measurement) => ({
x: measurement.time,
y: measurement.value,
locationId: measurement.locationId,
})),
pointRadius: 0,
borderColor: sensor.color,
Expand All @@ -210,6 +232,7 @@ export default function Graph({
data: sensor.data.map((measurement) => ({
x: measurement.time,
y: measurement.min_value,
locationId: null,
})),
borderColor: sensor.color + "33",
backgroundColor: sensor.color + "33",
Expand All @@ -222,6 +245,7 @@ export default function Graph({
data: sensor.data.map((measurement) => ({
x: measurement.time,
y: measurement.max_value,
locationId: null,
})),
borderColor: sensor.color + "33",
backgroundColor: sensor.color + "33",
Expand Down Expand Up @@ -275,6 +299,8 @@ export default function Graph({
// locale: data.locale === "de" ? de : enGB,
// },
// },
min: currentZoom?.xMin,
max: currentZoom?.xMax,
ticks: {
major: {
enabled: true,
Expand Down Expand Up @@ -324,21 +350,38 @@ export default function Graph({
},
},
plugins: {
zoom: {
pan: {
enabled: true,
mode: "xy",
onPan: () => setIsZoomed(true), // Mark zoom as active
tooltip: {
enabled: true,
mode: "index",
intersect: false,
callbacks: {
label: (context: any) => {
const dataIndex = context.dataIndex;
const datasetIndex = context.datasetIndex;
const point = lineData.datasets[datasetIndex].data[dataIndex];
const locationId = point.locationId;
setHoveredPoint(locationId);
return `${context.dataset.label}: ${context.raw.y}`;
},
},
},
zoom: {
zoom: {
wheel: {
enabled: true,
},
pinch: {
drag: {
enabled: true,
},
mode: "xy",
onZoom: () => setIsZoomed(true), // Mark zoom as active
mode: "x",
onZoom: ({ chart }) => {
const xScale = chart.scales["x"];
const xMin = xScale.min;
const xMax = xScale.max;

// Track the zoom level
setCurrentZoom({ xMin, xMax });
},
},
},
legend: {
Expand Down Expand Up @@ -376,11 +419,13 @@ export default function Graph({
}, [
startDate,
endDate,
// data.locale,
sensors,
currentZoom?.xMin,
currentZoom?.xMax,
theme,
colorPickerState.open,
sensors,
lineData.datasets,
setHoveredPoint,
colorPickerState.open,
]);

function handleColorChange(newColor: string) {
Expand Down Expand Up @@ -463,7 +508,7 @@ export default function Graph({
function handleResetZoomClick() {
if (chartRef.current) {
chartRef.current.resetZoom(); // Use the resetZoom function from the zoom plugin
setIsZoomed(false); // Reset zoom state
setCurrentZoom(null); // Reset the zoom state
}
}

Expand Down Expand Up @@ -500,21 +545,23 @@ export default function Graph({
<AggregationFilter />
</div>
<div className="flex items-center justify-end gap-4">
{isZoomed && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<RefreshCcw
onClick={handleResetZoomClick}
className="cursor-pointer"
/>
</TooltipTrigger>
<TooltipContent>
<p>Reset zoom</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{currentZoom !== null &&
currentZoom.xMax !== 0 &&
currentZoom.xMin !== 0 && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<RefreshCcw
onClick={handleResetZoomClick}
className="cursor-pointer"
/>
</TooltipTrigger>
<TooltipContent>
<p>Reset zoom</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
<DropdownMenu>
<DropdownMenuTrigger>
<Download />
Expand Down Expand Up @@ -552,7 +599,7 @@ export default function Graph({
) : (
<ClientOnly fallback={<Spinner />}>
{() => (
<LineWithZoom
<GraphWithZoom
lineData={lineData}
options={options}
chartRef={chartRef} // Pass chartRef as a prop
Expand Down
Loading

0 comments on commit 4b18bc2

Please sign in to comment.