mirror of
https://github.com/ForFarmTeam/ForFarm.git
synced 2025-12-19 14:04:08 +01:00
feat: modify google map state management + add farm creation
This commit is contained in:
parent
a5bdac4f2d
commit
89364fae0c
@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/danielgtaylor/huma/v2"
|
"github.com/danielgtaylor/huma/v2"
|
||||||
@ -58,11 +59,11 @@ func (a *api) registerFarmRoutes(_ chi.Router, api huma.API) {
|
|||||||
type CreateFarmInput struct {
|
type CreateFarmInput struct {
|
||||||
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
Header string `header:"Authorization" required:"true" example:"Bearer token"`
|
||||||
Body struct {
|
Body struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"Name"`
|
||||||
Lat float64 `json:"lat"`
|
Lat float64 `json:"Lat"`
|
||||||
Lon float64 `json:"lon"`
|
Lon float64 `json:"Lon"`
|
||||||
FarmType string `json:"farm_type,omitempty"`
|
FarmType string `json:"FarmType,omitempty"`
|
||||||
TotalSize string `json:"total_size,omitempty"`
|
TotalSize string `json:"TotalSize,omitempty"`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,7 +136,7 @@ func (a *api) createFarmHandler(ctx context.Context, input *CreateFarmInput) (*C
|
|||||||
TotalSize: input.Body.TotalSize,
|
TotalSize: input.Body.TotalSize,
|
||||||
OwnerID: userID,
|
OwnerID: userID,
|
||||||
}
|
}
|
||||||
|
fmt.Println(farm)
|
||||||
if err := a.farmRepo.CreateOrUpdate(ctx, farm); err != nil {
|
if err := a.farmRepo.CreateOrUpdate(ctx, farm); err != nil {
|
||||||
return nil, huma.Error500InternalServerError("failed to create farm", err)
|
return nil, huma.Error500InternalServerError("failed to create farm", err)
|
||||||
}
|
}
|
||||||
|
|||||||
33
frontend/api/crop.ts
Normal file
33
frontend/api/crop.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import axiosInstance from "./config";
|
||||||
|
import type { Cropland } from "@/types";
|
||||||
|
|
||||||
|
export interface CropResponse {
|
||||||
|
croplands: Cropland[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a specific Crop by FarmID.
|
||||||
|
* Calls GET /crop/farm/{farm_id} and returns fallback data on failure.
|
||||||
|
*/
|
||||||
|
export async function getCrop(farmId: string): Promise<CropResponse> {
|
||||||
|
return axiosInstance.get<CropResponse>(`/crop/farm/${farmId}`).then((res) => res.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// body
|
||||||
|
// {
|
||||||
|
// "farm_id": "string",
|
||||||
|
// "growth_stage": "string",
|
||||||
|
// "land_size": 0,
|
||||||
|
// "name": "string",
|
||||||
|
// "plant_id": "string",
|
||||||
|
// "priority": 0,
|
||||||
|
// "status": "string",
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new crop by FarmID.
|
||||||
|
* Calls POST /crop and returns fallback data on failure.
|
||||||
|
*/
|
||||||
|
export async function createCrop(data: Partial<Cropland>): Promise<Cropland> {
|
||||||
|
return axiosInstance.post<Cropland>(`/crop`, data).then((res) => res.data);
|
||||||
|
}
|
||||||
@ -22,27 +22,7 @@ export async function createFarm(data: Partial<Farm>): Promise<Farm> {
|
|||||||
* Calls GET /farms/{farm_id} and returns fallback data on failure.
|
* Calls GET /farms/{farm_id} and returns fallback data on failure.
|
||||||
*/
|
*/
|
||||||
export async function getFarm(farmId: string): Promise<Farm> {
|
export async function getFarm(farmId: string): Promise<Farm> {
|
||||||
// Simulate a network delay.
|
return axiosInstance.get<Farm>(`/farms/${farmId}`).then((res) => res.data);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axiosInstance.get<Farm>(`/farms/${farmId}`);
|
|
||||||
return response.data;
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`Error fetching farm ${farmId}. Returning fallback data:`, error);
|
|
||||||
const dummyDate = new Date().toISOString();
|
|
||||||
return {
|
|
||||||
CreatedAt: dummyDate,
|
|
||||||
FarmType: "conventional",
|
|
||||||
Lat: 15.87,
|
|
||||||
Lon: 100.9925,
|
|
||||||
Name: "Fallback Farm",
|
|
||||||
OwnerID: "fallback_owner",
|
|
||||||
TotalSize: "40 hectares",
|
|
||||||
UUID: farmId,
|
|
||||||
UpdatedAt: dummyDate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
10
frontend/api/plant.ts
Normal file
10
frontend/api/plant.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import axiosInstance from "./config";
|
||||||
|
import type { Plant } from "@/types";
|
||||||
|
|
||||||
|
export interface PlantResponse {
|
||||||
|
plants: Plant[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlants(): Promise<PlantResponse> {
|
||||||
|
return axiosInstance.get<PlantResponse>("/plant").then((res) => res.data);
|
||||||
|
}
|
||||||
@ -7,38 +7,66 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
||||||
import { useState } from "react";
|
import { useState, useCallback } from "react";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
import type { Farm } from "@/types";
|
import type { Farm } from "@/types";
|
||||||
import GoogleMapWithDrawing from "@/components/google-map-with-drawing";
|
import GoogleMapWithDrawing, { type ShapeData } from "@/components/google-map-with-drawing";
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Schema Definition: Validates form inputs using Zod
|
||||||
|
// See: https://zod.dev
|
||||||
|
// ===================================================================
|
||||||
const farmFormSchema = z.object({
|
const farmFormSchema = z.object({
|
||||||
name: z.string().min(2, "Farm name must be at least 2 characters"),
|
name: z.string().min(2, "Farm name must be at least 2 characters"),
|
||||||
latitude: z.number().min(-90, "Invalid latitude").max(90, "Invalid latitude"),
|
latitude: z
|
||||||
longitude: z.number().min(-180, "Invalid longitude").max(180, "Invalid longitude"),
|
.number({ invalid_type_error: "Latitude must be a number" })
|
||||||
|
.min(-90, "Invalid latitude")
|
||||||
|
.max(90, "Invalid latitude")
|
||||||
|
.refine((val) => val !== 0, { message: "Please select a location on the map." }),
|
||||||
|
longitude: z
|
||||||
|
.number({ invalid_type_error: "Longitude must be a number" })
|
||||||
|
.min(-180, "Invalid longitude")
|
||||||
|
.max(180, "Invalid longitude")
|
||||||
|
.refine((val) => val !== 0, { message: "Please select a location on the map." }),
|
||||||
type: z.string().min(1, "Please select a farm type"),
|
type: z.string().min(1, "Please select a farm type"),
|
||||||
area: z.string().optional(),
|
area: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Component Props Declaration
|
||||||
|
// ===================================================================
|
||||||
export interface AddFarmFormProps {
|
export interface AddFarmFormProps {
|
||||||
onSubmit: (data: Partial<Farm>) => Promise<void>;
|
onSubmit: (data: Partial<Farm>) => Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Component: AddFarmForm
|
||||||
|
// - Manages the creation of new farm records.
|
||||||
|
// - Uses React Hook Form with Zod for form validation.
|
||||||
|
// - Includes a map component for coordinate selection.
|
||||||
|
// ===================================================================
|
||||||
export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
|
export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// State and Form Setup
|
||||||
|
// ---------------------------------------------------------------
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof farmFormSchema>>({
|
const form = useForm<z.infer<typeof farmFormSchema>>({
|
||||||
resolver: zodResolver(farmFormSchema),
|
resolver: zodResolver(farmFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
latitude: 0,
|
latitude: 0, // Defaults handled by validation (marker must be selected)
|
||||||
longitude: 0,
|
longitude: 0,
|
||||||
type: "",
|
type: "",
|
||||||
area: "",
|
area: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// Form Submission Handler
|
||||||
|
// - Converts form data to the expected Farm shape.
|
||||||
|
// ---------------------------------------------------------------
|
||||||
const handleSubmit = async (values: z.infer<typeof farmFormSchema>) => {
|
const handleSubmit = async (values: z.infer<typeof farmFormSchema>) => {
|
||||||
try {
|
try {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
@ -58,20 +86,41 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAreaSelected = (coordinates: { lat: number; lng: number }[]) => {
|
// ---------------------------------------------------------------
|
||||||
if (coordinates.length > 0) {
|
// Map-to-Form Coordination: Update coordinates from the map
|
||||||
const { lat, lng } = coordinates[0];
|
// - Uses useCallback to preserve reference and optimize re-renders.
|
||||||
form.setValue("latitude", lat);
|
// ---------------------------------------------------------------
|
||||||
form.setValue("longitude", lng);
|
const handleShapeDrawn = useCallback(
|
||||||
}
|
(data: ShapeData) => {
|
||||||
};
|
// Log incoming shape data for debugging
|
||||||
|
console.log("Shape drawn in form:", data);
|
||||||
|
|
||||||
|
// Only update the form if a single marker (i.e. point) is used
|
||||||
|
if (data.type === "marker") {
|
||||||
|
const { lat, lng } = data.position;
|
||||||
|
form.setValue("latitude", lat, { shouldValidate: true });
|
||||||
|
form.setValue("longitude", lng, { shouldValidate: true });
|
||||||
|
console.log(`Set form lat: ${lat}, lng: ${lng}`);
|
||||||
|
} else {
|
||||||
|
// Note: Only markers update coordinates. Other shapes could be handled later.
|
||||||
|
console.log(`Received shape type '${data.type}', but only 'marker' updates the form coordinates.`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[form]
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===================================================================
|
||||||
|
// Render: Split into two main sections - Form and Map
|
||||||
|
// ===================================================================
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col lg:flex-row gap-6">
|
<div className="flex flex-col lg:flex-row gap-6 p-4">
|
||||||
{/* Form Section */}
|
{/* ==============================
|
||||||
<div className="flex-1">
|
Start of Form Section
|
||||||
|
============================== */}
|
||||||
|
<div className="lg:flex-[1]">
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||||
|
{/* Farm Name Field */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
@ -87,34 +136,51 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
{/* Coordinate Fields (Latitude & Longitude) */}
|
||||||
control={form.control}
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
name="latitude"
|
<FormField
|
||||||
render={({ field }) => (
|
control={form.control}
|
||||||
<FormItem>
|
name="latitude"
|
||||||
<FormLabel>Latitude</FormLabel>
|
render={({ field }) => (
|
||||||
<FormControl>
|
<FormItem>
|
||||||
<Input placeholder="Latitude" {...field} disabled />
|
<FormLabel>Latitude</FormLabel>
|
||||||
</FormControl>
|
<FormControl>
|
||||||
<FormMessage />
|
<Input
|
||||||
</FormItem>
|
placeholder="Select on map"
|
||||||
)}
|
{...field}
|
||||||
/>
|
value={field.value ? field.value.toFixed(6) : ""}
|
||||||
|
disabled
|
||||||
<FormField
|
readOnly
|
||||||
control={form.control}
|
className="disabled:opacity-100 disabled:cursor-default"
|
||||||
name="longitude"
|
/>
|
||||||
render={({ field }) => (
|
</FormControl>
|
||||||
<FormItem>
|
<FormMessage />
|
||||||
<FormLabel>Longitude</FormLabel>
|
</FormItem>
|
||||||
<FormControl>
|
)}
|
||||||
<Input placeholder="Longitude" {...field} disabled />
|
/>
|
||||||
</FormControl>
|
<FormField
|
||||||
<FormMessage />
|
control={form.control}
|
||||||
</FormItem>
|
name="longitude"
|
||||||
)}
|
render={({ field }) => (
|
||||||
/>
|
<FormItem>
|
||||||
|
<FormLabel>Longitude</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
placeholder="Select on map"
|
||||||
|
{...field}
|
||||||
|
value={field.value ? field.value.toFixed(6) : ""}
|
||||||
|
disabled
|
||||||
|
readOnly
|
||||||
|
className="disabled:opacity-100 disabled:cursor-default"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Farm Type Selection */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="type"
|
name="type"
|
||||||
@ -140,6 +206,7 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Total Area Field */}
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="area"
|
name="area"
|
||||||
@ -147,15 +214,18 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Total Area (optional)</FormLabel>
|
<FormLabel>Total Area (optional)</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input placeholder="e.g., 10 hectares" {...field} />
|
<Input placeholder="e.g., 10 hectares" {...field} value={field.value ?? ""} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>The total size of your farm</FormDescription>
|
<FormDescription>
|
||||||
|
The total size of your farm (e.g., "15 rai", "10 hectares").
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex justify-end gap-2">
|
{/* Submit and Cancel Buttons */}
|
||||||
|
<div className="flex justify-end gap-2 pt-4">
|
||||||
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
<Button type="button" variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@ -173,12 +243,27 @@ export function AddFarmForm({ onSubmit, onCancel }: AddFarmFormProps) {
|
|||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
{/* ==============================
|
||||||
|
End of Form Section
|
||||||
|
============================== */}
|
||||||
|
|
||||||
{/* Map Section */}
|
{/* ==============================
|
||||||
<div className="flex-1">
|
Start of Map Section
|
||||||
<FormLabel>Farm Location</FormLabel>
|
- Renders an interactive map for coordinate selection.
|
||||||
<GoogleMapWithDrawing onAreaSelected={handleAreaSelected} />
|
============================== */}
|
||||||
|
<div className="lg:flex-[2] min-h-[400px] lg:min-h-0 flex flex-col">
|
||||||
|
<FormLabel>Farm Location (Draw marker on map)</FormLabel>
|
||||||
|
<div className="mt-2 rounded-md overflow-hidden border flex-grow">
|
||||||
|
<GoogleMapWithDrawing onShapeDrawn={handleShapeDrawn} />
|
||||||
|
</div>
|
||||||
|
<FormDescription className="mt-2">
|
||||||
|
Select the marker tool above the map and click a location to set the latitude and longitude for your farm.
|
||||||
|
Only markers will update the coordinates.
|
||||||
|
</FormDescription>
|
||||||
</div>
|
</div>
|
||||||
|
{/* ==============================
|
||||||
|
End of Map Section
|
||||||
|
============================== */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export function FarmCard({ variant, farm, onClick }: FarmCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="bg-muted/30 dark:bg-muted/20 rounded-md p-2 text-center">
|
<div className="bg-muted/30 dark:bg-muted/20 rounded-md p-2 text-center">
|
||||||
<p className="text-xs text-muted-foreground">Crops</p>
|
<p className="text-xs text-muted-foreground">Crops</p>
|
||||||
<p className="font-medium">{farm.crops}</p>
|
<p className="font-medium">{farm.Crops ? farm.Crops.length : 0}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { extractRoute } from "@/lib/utils";
|
|||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Toaster } from "@/components/ui/sonner";
|
import { Toaster } from "@/components/ui/sonner";
|
||||||
import { useForm, FormProvider } from "react-hook-form";
|
import { useForm, FormProvider } from "react-hook-form";
|
||||||
|
import { APIProvider } from "@vis.gl/react-google-maps";
|
||||||
|
|
||||||
export default function AppLayout({
|
export default function AppLayout({
|
||||||
children,
|
children,
|
||||||
@ -20,22 +21,24 @@ export default function AppLayout({
|
|||||||
const form = useForm();
|
const form = useForm();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<APIProvider apiKey={process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!}>
|
||||||
<AppSidebar />
|
<SidebarProvider>
|
||||||
<SidebarInset>
|
<AppSidebar />
|
||||||
<FormProvider {...form}>
|
<SidebarInset>
|
||||||
<header className="flex h-16 shrink-0 items-center gap-2">
|
<FormProvider {...form}>
|
||||||
<div className="flex items-center gap-2 px-4">
|
<header className="flex h-16 shrink-0 items-center gap-2">
|
||||||
<SidebarTrigger className="-ml-1" />
|
<div className="flex items-center gap-2 px-4">
|
||||||
<ThemeToggle />
|
<SidebarTrigger className="-ml-1" />
|
||||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
<ThemeToggle />
|
||||||
<DynamicBreadcrumb pathname={currentPathname} />
|
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||||
</div>
|
<DynamicBreadcrumb pathname={currentPathname} />
|
||||||
</header>
|
</div>
|
||||||
{children}
|
</header>
|
||||||
<Toaster />
|
{children}
|
||||||
</FormProvider>
|
<Toaster />
|
||||||
</SidebarInset>
|
</FormProvider>
|
||||||
</SidebarProvider>
|
</SidebarInset>
|
||||||
|
</SidebarProvider>
|
||||||
|
</APIProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,78 +1,51 @@
|
|||||||
import { GoogleMap, LoadScript, DrawingManager } from "@react-google-maps/api";
|
// google-map-with-drawing.tsx
|
||||||
import { useState, useCallback } from "react";
|
import React from "react";
|
||||||
|
import { ControlPosition, Map, MapControl } from "@vis.gl/react-google-maps";
|
||||||
|
|
||||||
const containerStyle = {
|
import { UndoRedoControl } from "@/components/map-component/undo-redo-control";
|
||||||
width: "100%",
|
// Import ShapeData and useDrawingManager from the correct path
|
||||||
height: "500px",
|
import { useDrawingManager, type ShapeData } from "@/components/map-component/use-drawing-manager"; // Adjust path if needed
|
||||||
};
|
|
||||||
|
|
||||||
const center = { lat: 13.7563, lng: 100.5018 }; // Example: Bangkok, Thailand
|
// Export the type so the form can use it
|
||||||
|
export { type ShapeData };
|
||||||
|
|
||||||
|
// Define props for the component
|
||||||
interface GoogleMapWithDrawingProps {
|
interface GoogleMapWithDrawingProps {
|
||||||
onAreaSelected: (data: { lat: number; lng: number }[]) => void;
|
onShapeDrawn: (data: ShapeData) => void; // Callback prop
|
||||||
|
// Add any other props you might need, e.g., initialCenter, initialZoom
|
||||||
|
initialCenter?: { lat: number; lng: number };
|
||||||
|
initialZoom?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rename DrawingExample to GoogleMapWithDrawing and accept props
|
||||||
const GoogleMapWithDrawing = ({
|
const GoogleMapWithDrawing = ({
|
||||||
onAreaSelected,
|
onShapeDrawn, // Destructure the callback prop
|
||||||
|
initialCenter = { lat: 13.7563, lng: 100.5018 }, // Default center
|
||||||
|
initialZoom = 10, // Default zoom
|
||||||
}: GoogleMapWithDrawingProps) => {
|
}: GoogleMapWithDrawingProps) => {
|
||||||
const [map, setMap] = useState<google.maps.Map | null>(null);
|
// Pass the onShapeDrawn callback directly to the hook
|
||||||
|
const drawingManager = useDrawingManager(onShapeDrawn);
|
||||||
const onDrawingComplete = useCallback(
|
|
||||||
(overlay: google.maps.drawing.OverlayCompleteEvent) => {
|
|
||||||
const shape = overlay.overlay;
|
|
||||||
|
|
||||||
if (shape instanceof google.maps.Polyline) {
|
|
||||||
const path = shape.getPath();
|
|
||||||
const coordinates = path.getArray().map((latLng) => ({
|
|
||||||
lat: latLng.lat(),
|
|
||||||
lng: latLng.lng(),
|
|
||||||
}));
|
|
||||||
// console.log("Polyline coordinates:", coordinates);
|
|
||||||
onAreaSelected(coordinates);
|
|
||||||
} else {
|
|
||||||
console.log("Unknown shape detected:", shape);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onAreaSelected]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoadScript
|
<>
|
||||||
googleMapsApiKey={process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!}
|
{/* Use props for map defaults */}
|
||||||
libraries={["drawing"]}
|
<Map
|
||||||
>
|
defaultZoom={initialZoom}
|
||||||
<GoogleMap
|
defaultCenter={initialCenter}
|
||||||
mapContainerStyle={containerStyle}
|
gestureHandling={"greedy"}
|
||||||
center={center}
|
disableDefaultUI={true}
|
||||||
zoom={10}
|
mapId={"YOUR_MAP_ID"} // Recommended: Add a Map ID
|
||||||
onLoad={(map) => setMap(map)}
|
/>
|
||||||
>
|
|
||||||
{map && (
|
{/* Render controls only if drawingManager is available */}
|
||||||
<DrawingManager
|
{drawingManager && (
|
||||||
onOverlayComplete={onDrawingComplete}
|
<MapControl position={ControlPosition.TOP_LEFT}>
|
||||||
options={{
|
{/* Pass drawingManager to UndoRedoControl */}
|
||||||
drawingControl: true,
|
<UndoRedoControl drawingManager={drawingManager} />
|
||||||
drawingControlOptions: {
|
</MapControl>
|
||||||
position: google.maps.ControlPosition.TOP_CENTER,
|
)}
|
||||||
drawingModes: [
|
{/* The drawing controls (marker, polygon etc.) are added by useDrawingManager */}
|
||||||
google.maps.drawing.OverlayType.POLYLINE,
|
</>
|
||||||
],
|
|
||||||
},
|
|
||||||
polygonOptions: {
|
|
||||||
fillColor: "#FF0000",
|
|
||||||
fillOpacity: 0.5,
|
|
||||||
strokeWeight: 2,
|
|
||||||
},
|
|
||||||
rectangleOptions: {
|
|
||||||
fillColor: "#00FF00",
|
|
||||||
fillOpacity: 0.5,
|
|
||||||
strokeWeight: 2,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</GoogleMap>
|
|
||||||
</LoadScript>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
45
frontend/components/map-component/undo-redo-control.tsx
Normal file
45
frontend/components/map-component/undo-redo-control.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import React, { useReducer, useRef } from "react";
|
||||||
|
import { useMap } from "@vis.gl/react-google-maps";
|
||||||
|
|
||||||
|
import reducer, { useDrawingManagerEvents, useOverlaySnapshots } from "@/components/map-component/undo-redo";
|
||||||
|
|
||||||
|
import { DrawingActionKind } from "@/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
drawingManager: google.maps.drawing.DrawingManager | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UndoRedoControl = ({ drawingManager }: Props) => {
|
||||||
|
const map = useMap();
|
||||||
|
|
||||||
|
const [state, dispatch] = useReducer(reducer, {
|
||||||
|
past: [],
|
||||||
|
now: [],
|
||||||
|
future: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// We need this ref to prevent infinite loops in certain cases.
|
||||||
|
// For example when the radius of circle is set via code (and not by user interaction)
|
||||||
|
// the radius_changed event gets triggered again. This would cause an infinite loop.
|
||||||
|
// This solution can be improved by comparing old vs. new values. For now we turn
|
||||||
|
// off the "updating" when snapshot changes are applied back to the overlays.
|
||||||
|
const overlaysShouldUpdateRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
useDrawingManagerEvents(drawingManager, overlaysShouldUpdateRef, dispatch);
|
||||||
|
useOverlaySnapshots(map, state, overlaysShouldUpdateRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="drawing-history">
|
||||||
|
<button onClick={() => dispatch({ type: DrawingActionKind.UNDO })} disabled={!state.past.length}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
|
||||||
|
<path d="M280-200v-80h284q63 0 109.5-40T720-420q0-60-46.5-100T564-560H312l104 104-56 56-200-200 200-200 56 56-104 104h252q97 0 166.5 63T800-420q0 94-69.5 157T564-200H280Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button onClick={() => dispatch({ type: DrawingActionKind.REDO })} disabled={!state.future.length}>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24">
|
||||||
|
<path d="M396-200q-97 0-166.5-63T160-420q0-94 69.5-157T396-640h252L544-744l56-56 200 200-200 200-56-56 104-104H396q-63 0-109.5 40T240-420q0 60 46.5 100T396-280h284v80H396Z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
217
frontend/components/map-component/undo-redo.ts
Normal file
217
frontend/components/map-component/undo-redo.ts
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import { Dispatch, MutableRefObject, useEffect } from "react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
DrawResult,
|
||||||
|
DrawingActionKind,
|
||||||
|
Overlay,
|
||||||
|
Snapshot,
|
||||||
|
State,
|
||||||
|
isCircle,
|
||||||
|
isMarker,
|
||||||
|
isPolygon,
|
||||||
|
isPolyline,
|
||||||
|
isRectangle,
|
||||||
|
} from "@/types";
|
||||||
|
|
||||||
|
export default function reducer(state: State, action: Action) {
|
||||||
|
switch (action.type) {
|
||||||
|
// This action is called whenever anything changes on any overlay.
|
||||||
|
// We then take a snapshot of the relevant values of each overlay and
|
||||||
|
// save them as the new "now". The old "now" is added to the "past" stack
|
||||||
|
case DrawingActionKind.UPDATE_OVERLAYS: {
|
||||||
|
const overlays = state.now.map((overlay: Overlay) => {
|
||||||
|
const snapshot: Snapshot = {};
|
||||||
|
const { geometry } = overlay;
|
||||||
|
|
||||||
|
if (isCircle(geometry)) {
|
||||||
|
snapshot.center = geometry.getCenter()?.toJSON();
|
||||||
|
snapshot.radius = geometry.getRadius();
|
||||||
|
} else if (isMarker(geometry)) {
|
||||||
|
snapshot.position = geometry.getPosition()?.toJSON();
|
||||||
|
} else if (isPolygon(geometry) || isPolyline(geometry)) {
|
||||||
|
snapshot.path = geometry.getPath()?.getArray();
|
||||||
|
} else if (isRectangle(geometry)) {
|
||||||
|
snapshot.bounds = geometry.getBounds()?.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...overlay,
|
||||||
|
snapshot,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
now: [...overlays],
|
||||||
|
past: [...state.past, state.now],
|
||||||
|
future: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// This action is called when a new overlay is added to the map.
|
||||||
|
// We then take a snapshot of the relevant values of the new overlay and
|
||||||
|
// add it to the "now" state. The old "now" is added to the "past" stack
|
||||||
|
case DrawingActionKind.SET_OVERLAY: {
|
||||||
|
const { overlay } = action.payload;
|
||||||
|
|
||||||
|
const snapshot: Snapshot = {};
|
||||||
|
|
||||||
|
if (isCircle(overlay)) {
|
||||||
|
snapshot.center = overlay.getCenter()?.toJSON();
|
||||||
|
snapshot.radius = overlay.getRadius();
|
||||||
|
} else if (isMarker(overlay)) {
|
||||||
|
snapshot.position = overlay.getPosition()?.toJSON();
|
||||||
|
} else if (isPolygon(overlay) || isPolyline(overlay)) {
|
||||||
|
snapshot.path = overlay.getPath()?.getArray();
|
||||||
|
} else if (isRectangle(overlay)) {
|
||||||
|
snapshot.bounds = overlay.getBounds()?.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
past: [...state.past, state.now],
|
||||||
|
now: [
|
||||||
|
...state.now,
|
||||||
|
{
|
||||||
|
type: action.payload.type,
|
||||||
|
geometry: action.payload.overlay,
|
||||||
|
snapshot,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
future: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// This action is called when the undo button is clicked.
|
||||||
|
// Get the top item from the "past" stack and set it as the new "now".
|
||||||
|
// Add the old "now" to the "future" stack to enable redo functionality
|
||||||
|
case DrawingActionKind.UNDO: {
|
||||||
|
const last = state.past.slice(-1)[0];
|
||||||
|
|
||||||
|
if (!last) return state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
past: [...state.past].slice(0, -1),
|
||||||
|
now: last,
|
||||||
|
future: state.now ? [...state.future, state.now] : state.future,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// This action is called when the redo button is clicked.
|
||||||
|
// Get the top item from the "future" stack and set it as the new "now".
|
||||||
|
// Add the old "now" to the "past" stack to enable undo functionality
|
||||||
|
case DrawingActionKind.REDO: {
|
||||||
|
const next = state.future.slice(-1)[0];
|
||||||
|
|
||||||
|
if (!next) return state;
|
||||||
|
|
||||||
|
return {
|
||||||
|
past: state.now ? [...state.past, state.now] : state.past,
|
||||||
|
now: next,
|
||||||
|
future: [...state.future].slice(0, -1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle drawing manager events
|
||||||
|
export function useDrawingManagerEvents(
|
||||||
|
drawingManager: google.maps.drawing.DrawingManager | null,
|
||||||
|
overlaysShouldUpdateRef: MutableRefObject<boolean>,
|
||||||
|
dispatch: Dispatch<Action>
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!drawingManager) return;
|
||||||
|
|
||||||
|
const eventListeners: Array<google.maps.MapsEventListener> = [];
|
||||||
|
|
||||||
|
const addUpdateListener = (eventName: string, drawResult: DrawResult) => {
|
||||||
|
const updateListener = google.maps.event.addListener(drawResult.overlay, eventName, () => {
|
||||||
|
if (eventName === "dragstart") {
|
||||||
|
overlaysShouldUpdateRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventName === "dragend") {
|
||||||
|
overlaysShouldUpdateRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overlaysShouldUpdateRef.current) {
|
||||||
|
dispatch({ type: DrawingActionKind.UPDATE_OVERLAYS });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventListeners.push(updateListener);
|
||||||
|
};
|
||||||
|
|
||||||
|
const overlayCompleteListener = google.maps.event.addListener(
|
||||||
|
drawingManager,
|
||||||
|
"overlaycomplete",
|
||||||
|
(drawResult: DrawResult) => {
|
||||||
|
switch (drawResult.type) {
|
||||||
|
case google.maps.drawing.OverlayType.CIRCLE:
|
||||||
|
["center_changed", "radius_changed"].forEach((eventName) => addUpdateListener(eventName, drawResult));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case google.maps.drawing.OverlayType.MARKER:
|
||||||
|
["dragend"].forEach((eventName) => addUpdateListener(eventName, drawResult));
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case google.maps.drawing.OverlayType.POLYGON:
|
||||||
|
case google.maps.drawing.OverlayType.POLYLINE:
|
||||||
|
["mouseup"].forEach((eventName) => addUpdateListener(eventName, drawResult));
|
||||||
|
|
||||||
|
case google.maps.drawing.OverlayType.RECTANGLE:
|
||||||
|
["bounds_changed", "dragstart", "dragend"].forEach((eventName) => addUpdateListener(eventName, drawResult));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({ type: DrawingActionKind.SET_OVERLAY, payload: drawResult });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
eventListeners.push(overlayCompleteListener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
eventListeners.forEach((listener) => google.maps.event.removeListener(listener));
|
||||||
|
};
|
||||||
|
}, [dispatch, drawingManager, overlaysShouldUpdateRef]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update overlays with the current "snapshot" when the "now" state changes
|
||||||
|
export function useOverlaySnapshots(
|
||||||
|
map: google.maps.Map | null,
|
||||||
|
state: State,
|
||||||
|
overlaysShouldUpdateRef: MutableRefObject<boolean>
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!map || !state.now) return;
|
||||||
|
|
||||||
|
for (const overlay of state.now) {
|
||||||
|
overlaysShouldUpdateRef.current = false;
|
||||||
|
|
||||||
|
overlay.geometry.setMap(map);
|
||||||
|
|
||||||
|
const { radius, center, position, path, bounds } = overlay.snapshot;
|
||||||
|
|
||||||
|
if (isCircle(overlay.geometry)) {
|
||||||
|
overlay.geometry.setRadius(radius ?? 0);
|
||||||
|
overlay.geometry.setCenter(center ?? null);
|
||||||
|
} else if (isMarker(overlay.geometry)) {
|
||||||
|
overlay.geometry.setPosition(position);
|
||||||
|
} else if (isPolygon(overlay.geometry) || isPolyline(overlay.geometry)) {
|
||||||
|
overlay.geometry.setPath(path ?? []);
|
||||||
|
} else if (isRectangle(overlay.geometry)) {
|
||||||
|
overlay.geometry.setBounds(bounds ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
overlaysShouldUpdateRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const overlay of state.now) {
|
||||||
|
overlay.geometry.setMap(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [map, overlaysShouldUpdateRef, state.now]);
|
||||||
|
}
|
||||||
139
frontend/components/map-component/use-drawing-manager.tsx
Normal file
139
frontend/components/map-component/use-drawing-manager.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
// use-drawing-manager.tsx
|
||||||
|
import { useMap, useMapsLibrary } from "@vis.gl/react-google-maps";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
// Define types for the data we'll pass back
|
||||||
|
type MarkerData = { type: "marker"; position: { lat: number; lng: number } };
|
||||||
|
type PolygonData = { type: "polygon"; path: { lat: number; lng: number }[] };
|
||||||
|
type PolylineData = { type: "polyline"; path: { lat: number; lng: number }[] };
|
||||||
|
// Add other types (Rectangle, Circle) if you enable them
|
||||||
|
// type RectangleData = { type: 'rectangle'; bounds: { north: number; east: number; south: number; west: number } };
|
||||||
|
// type CircleData = { type: 'circle'; center: { lat: number; lng: number }; radius: number };
|
||||||
|
|
||||||
|
export type ShapeData = MarkerData | PolygonData | PolylineData; // | RectangleData | CircleData;
|
||||||
|
|
||||||
|
// Add the callback function type to the hook's arguments
|
||||||
|
export function useDrawingManager(
|
||||||
|
onOverlayComplete?: (data: ShapeData) => void,
|
||||||
|
initialValue: google.maps.drawing.DrawingManager | null = null
|
||||||
|
) {
|
||||||
|
const map = useMap();
|
||||||
|
const drawing = useMapsLibrary("drawing");
|
||||||
|
|
||||||
|
const [drawingManager, setDrawingManager] = useState<google.maps.drawing.DrawingManager | null>(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!map || !drawing) return;
|
||||||
|
|
||||||
|
const newDrawingManager = new drawing.DrawingManager({
|
||||||
|
map,
|
||||||
|
// drawingMode: google.maps.drawing.OverlayType.MARKER, // You might want to set initial mode to null or let user choose
|
||||||
|
drawingMode: null, // Start without an active drawing mode
|
||||||
|
drawingControl: true,
|
||||||
|
drawingControlOptions: {
|
||||||
|
position: google.maps.ControlPosition.TOP_CENTER,
|
||||||
|
drawingModes: [
|
||||||
|
google.maps.drawing.OverlayType.MARKER,
|
||||||
|
// google.maps.drawing.OverlayType.CIRCLE,
|
||||||
|
google.maps.drawing.OverlayType.POLYGON,
|
||||||
|
google.maps.drawing.OverlayType.POLYLINE,
|
||||||
|
// google.maps.drawing.OverlayType.RECTANGLE,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
markerOptions: {
|
||||||
|
draggable: true,
|
||||||
|
},
|
||||||
|
// circleOptions: { // Uncomment if using circles
|
||||||
|
// editable: false,
|
||||||
|
// },
|
||||||
|
polygonOptions: {
|
||||||
|
editable: true,
|
||||||
|
draggable: true,
|
||||||
|
},
|
||||||
|
// rectangleOptions: { // Uncomment if using rectangles
|
||||||
|
// editable: true,
|
||||||
|
// draggable: true,
|
||||||
|
// },
|
||||||
|
polylineOptions: {
|
||||||
|
editable: true,
|
||||||
|
draggable: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setDrawingManager(newDrawingManager);
|
||||||
|
|
||||||
|
// --- Add Event Listener ---
|
||||||
|
const overlayCompleteListener = google.maps.event.addListener(
|
||||||
|
newDrawingManager,
|
||||||
|
"overlaycomplete",
|
||||||
|
(event: google.maps.drawing.OverlayCompleteEvent) => {
|
||||||
|
let data: ShapeData | null = null;
|
||||||
|
const overlay = event.overlay;
|
||||||
|
|
||||||
|
// Extract coordinates based on type
|
||||||
|
switch (event.type) {
|
||||||
|
case google.maps.drawing.OverlayType.MARKER:
|
||||||
|
const marker = overlay as google.maps.Marker;
|
||||||
|
const position = marker.getPosition();
|
||||||
|
if (position) {
|
||||||
|
data = {
|
||||||
|
type: "marker",
|
||||||
|
position: { lat: position.lat(), lng: position.lng() },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Optional: remove the drawn marker immediately if you only want the data
|
||||||
|
// marker.setMap(null);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case google.maps.drawing.OverlayType.POLYGON:
|
||||||
|
const polygon = overlay as google.maps.Polygon;
|
||||||
|
const path = polygon.getPath().getArray();
|
||||||
|
data = {
|
||||||
|
type: "polygon",
|
||||||
|
path: path.map((latLng) => ({ lat: latLng.lat(), lng: latLng.lng() })),
|
||||||
|
};
|
||||||
|
// Optional: remove the drawn polygon
|
||||||
|
// polygon.setMap(null);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case google.maps.drawing.OverlayType.POLYLINE:
|
||||||
|
const polyline = overlay as google.maps.Polyline;
|
||||||
|
const linePath = polyline.getPath().getArray();
|
||||||
|
data = {
|
||||||
|
type: "polyline",
|
||||||
|
path: linePath.map((latLng) => ({ lat: latLng.lat(), lng: latLng.lng() })),
|
||||||
|
};
|
||||||
|
// Optional: remove the drawn polyline
|
||||||
|
// polyline.setMap(null);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Add cases for RECTANGLE and CIRCLE if you enable them
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn("Unhandled overlay type:", event.type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the callback function if provided and data was extracted
|
||||||
|
if (data && onOverlayComplete) {
|
||||||
|
onOverlayComplete(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: Set drawing mode back to null after completion
|
||||||
|
// newDrawingManager.setDrawingMode(null);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// --- End Event Listener ---
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
// Remove the event listener
|
||||||
|
google.maps.event.removeListener(overlayCompleteListener);
|
||||||
|
// Remove the drawing manager from the map
|
||||||
|
newDrawingManager.setMap(null);
|
||||||
|
};
|
||||||
|
// Add onOverlayComplete to dependency array to ensure the latest callback is used
|
||||||
|
}, [map, drawing, onOverlayComplete]);
|
||||||
|
|
||||||
|
return drawingManager;
|
||||||
|
}
|
||||||
@ -31,6 +31,7 @@
|
|||||||
"@react-oauth/google": "^0.12.1",
|
"@react-oauth/google": "^0.12.1",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@tanstack/react-query": "^5.66.0",
|
"@tanstack/react-query": "^5.66.0",
|
||||||
|
"@vis.gl/react-google-maps": "^1.5.2",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
@ -74,6 +74,9 @@ importers:
|
|||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.66.0
|
specifier: ^5.66.0
|
||||||
version: 5.67.3(react@19.0.0)
|
version: 5.67.3(react@19.0.0)
|
||||||
|
'@vis.gl/react-google-maps':
|
||||||
|
specifier: ^1.5.2
|
||||||
|
version: 1.5.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.7.9
|
specifier: ^1.7.9
|
||||||
version: 1.8.3
|
version: 1.8.3
|
||||||
@ -1068,6 +1071,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==}
|
resolution: {integrity: sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
|
|
||||||
|
'@vis.gl/react-google-maps@1.5.2':
|
||||||
|
resolution: {integrity: sha512-0Ypmde7M73GgV4TgcaUTNKXsbcXWToPVuawMNrVg7htXmhpEfLARHwhtmP6N1da3od195ZKC8ShXzC6Vm+zYHQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0 || ^19.0 || ^19.0.0-rc'
|
||||||
|
react-dom: '>=16.8.0 || ^19.0 || ^19.0.0-rc'
|
||||||
|
|
||||||
acorn-jsx@5.3.2:
|
acorn-jsx@5.3.2:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3612,6 +3621,13 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.26.1
|
'@typescript-eslint/types': 8.26.1
|
||||||
eslint-visitor-keys: 4.2.0
|
eslint-visitor-keys: 4.2.0
|
||||||
|
|
||||||
|
'@vis.gl/react-google-maps@1.5.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||||
|
dependencies:
|
||||||
|
'@types/google.maps': 3.58.1
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
react: 19.0.0
|
||||||
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
|
||||||
acorn-jsx@5.3.2(acorn@8.14.1):
|
acorn-jsx@5.3.2(acorn@8.14.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.14.1
|
acorn: 8.14.1
|
||||||
|
|||||||
@ -1,3 +1,28 @@
|
|||||||
|
export interface Plant {
|
||||||
|
UUID: string;
|
||||||
|
Name: string;
|
||||||
|
Variety: string;
|
||||||
|
AverageHeight: number;
|
||||||
|
DaysToEmerge: number;
|
||||||
|
DaysToFlower: number;
|
||||||
|
DaysToMaturity: number;
|
||||||
|
EstimateLossRate: number;
|
||||||
|
EstimateRevenuePerHU: number;
|
||||||
|
HarvestUnitID: number;
|
||||||
|
HarvestWindow: number;
|
||||||
|
IsPerennial: boolean;
|
||||||
|
LightProfileID: number;
|
||||||
|
OptimalTemp: number;
|
||||||
|
PHValue: number;
|
||||||
|
PlantingDepth: number;
|
||||||
|
PlantingDetail: string;
|
||||||
|
RowSpacing: number;
|
||||||
|
SoilConditionID: number;
|
||||||
|
WaterNeeds: number;
|
||||||
|
CreatedAt: string;
|
||||||
|
UpdatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Crop {
|
export interface Crop {
|
||||||
id: string;
|
id: string;
|
||||||
farmId: string;
|
farmId: string;
|
||||||
@ -11,6 +36,19 @@ export interface Crop {
|
|||||||
progress?: number;
|
progress?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Cropland {
|
||||||
|
UUID: string;
|
||||||
|
Name: string;
|
||||||
|
Status: string;
|
||||||
|
Priority: number;
|
||||||
|
LandSize: number;
|
||||||
|
GrowthStage: string;
|
||||||
|
PlantID: string;
|
||||||
|
FarmID: string;
|
||||||
|
CreatedAt: Date;
|
||||||
|
UpdatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CropAnalytics {
|
export interface CropAnalytics {
|
||||||
cropId: string;
|
cropId: string;
|
||||||
growthProgress: number;
|
growthProgress: number;
|
||||||
@ -41,6 +79,7 @@ export interface Farm {
|
|||||||
TotalSize: string;
|
TotalSize: string;
|
||||||
UUID: string;
|
UUID: string;
|
||||||
UpdatedAt: Date;
|
UpdatedAt: Date;
|
||||||
|
Crops: Cropland[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// export interface Farm {
|
// export interface Farm {
|
||||||
@ -108,3 +147,75 @@ export interface Blog {
|
|||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------- Maps -----------$
|
||||||
|
|
||||||
|
export type OverlayGeometry =
|
||||||
|
| google.maps.Marker
|
||||||
|
| google.maps.Polygon
|
||||||
|
| google.maps.Polyline
|
||||||
|
| google.maps.Rectangle
|
||||||
|
| google.maps.Circle;
|
||||||
|
|
||||||
|
export interface DrawResult {
|
||||||
|
type: google.maps.drawing.OverlayType;
|
||||||
|
overlay: OverlayGeometry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Snapshot {
|
||||||
|
radius?: number;
|
||||||
|
center?: google.maps.LatLngLiteral;
|
||||||
|
position?: google.maps.LatLngLiteral;
|
||||||
|
path?: Array<google.maps.LatLng>;
|
||||||
|
bounds?: google.maps.LatLngBoundsLiteral;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Overlay {
|
||||||
|
type: google.maps.drawing.OverlayType;
|
||||||
|
geometry: OverlayGeometry;
|
||||||
|
snapshot: Snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface State {
|
||||||
|
past: Array<Array<Overlay>>;
|
||||||
|
now: Array<Overlay>;
|
||||||
|
future: Array<Array<Overlay>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DrawingActionKind {
|
||||||
|
SET_OVERLAY = "SET_OVERLAY",
|
||||||
|
UPDATE_OVERLAYS = "UPDATE_OVERLAYS",
|
||||||
|
UNDO = "UNDO",
|
||||||
|
REDO = "REDO",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionWithTypeOnly {
|
||||||
|
type: Exclude<DrawingActionKind, DrawingActionKind.SET_OVERLAY>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SetOverlayAction {
|
||||||
|
type: DrawingActionKind.SET_OVERLAY;
|
||||||
|
payload: DrawResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Action = ActionWithTypeOnly | SetOverlayAction;
|
||||||
|
|
||||||
|
export function isCircle(overlay: OverlayGeometry): overlay is google.maps.Circle {
|
||||||
|
return (overlay as google.maps.Circle).getCenter !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMarker(overlay: OverlayGeometry): overlay is google.maps.Marker {
|
||||||
|
return (overlay as google.maps.Marker).getPosition !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPolygon(overlay: OverlayGeometry): overlay is google.maps.Polygon {
|
||||||
|
return (overlay as google.maps.Polygon).getPath !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPolyline(overlay: OverlayGeometry): overlay is google.maps.Polyline {
|
||||||
|
return (overlay as google.maps.Polyline).getPath !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRectangle(overlay: OverlayGeometry): overlay is google.maps.Rectangle {
|
||||||
|
return (overlay as google.maps.Rectangle).getBounds !== undefined;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user