commit f98e2d3a7a1bbd6463fecbd46147ed3f99dee540 Author: Fred Boniface Date: Fri Aug 11 14:32:44 2023 +0100 Add imageing logic diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..499ac4b --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module git.fjla.uk/fred.boniface/map-dot + +go 1.19 + +require go.uber.org/zap v1.25.0 + +require go.uber.org/multierr v1.10.0 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c4b6d80 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/imaging/canvas.go b/imaging/canvas.go new file mode 100644 index 0000000..ffd71a4 --- /dev/null +++ b/imaging/canvas.go @@ -0,0 +1,22 @@ +package imaging + +import ( + "image" + "image/color" +) + +// CreateCanvas creates a new image canvas with the specified width and height +func createCanvas(width, height int) *image.RGBA { + canvas := image.NewRGBA(image.Rect(0, 0, width, height)) + clearCanvas(canvas, color.Black) + return canvas +} + +func clearCanvas(img *image.RGBA, backgroundColor color.Color) { + bounds := img.Bounds() + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + img.Set(x, y, backgroundColor) + } + } +} diff --git a/imaging/circles.go b/imaging/circles.go new file mode 100644 index 0000000..79583df --- /dev/null +++ b/imaging/circles.go @@ -0,0 +1 @@ +package imaging diff --git a/imaging/generate.go b/imaging/generate.go new file mode 100644 index 0000000..80e4928 --- /dev/null +++ b/imaging/generate.go @@ -0,0 +1,8 @@ +package imaging + +import "image" + +func Generate(height, width int, style, format string, data []LocationData) image.Image { + img := createCanvas(width, height) + return img +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..f385a50 --- /dev/null +++ b/log/log.go @@ -0,0 +1,36 @@ +package log + +import ( + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var Msg *zap.Logger + +func init() { + var err error + + // Create a custom configuration with a human-readable "Console" encoder + config := zap.NewDevelopmentConfig() + config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder // Adds color to log levels + + // Determine the log level based on the runtime mode + mode := os.Getenv("RUNTIME_MODE") + logLevel := zapcore.DebugLevel + if mode == "production" { + logLevel = zapcore.InfoLevel + } + + // Set the log level + config.Level = zap.NewAtomicLevelAt(logLevel) + + Msg, err = config.Build() // Potential source of the error + if err != nil { + panic("Failed to initialize logger: " + err.Error()) + } + + // Log the selected log level (optional, can be helpful for debugging) + Msg.Info("Log level set to: " + logLevel.String()) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..4ab7bfa --- /dev/null +++ b/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "git.fjla.uk/fred.boniface/map-dot/log" + "git.fjla.uk/fred.boniface/map-dot/run" +) + +var ( + showHelp bool +) + +func main() { + flag.BoolVar(&showHelp, "help", false, "Show extended help") + flag.Usage = customUsage + serverMode := flag.Bool("server", false, "Run as an API server - Omit all other flags if running as server") + height := flag.Uint64("height", 600, "Output image height") + width := flag.Uint64("width", 800, "Output image width") + style := flag.String("style", "circles", "Output image style") + format := flag.String("format", "png", "Output image format") + input := flag.String("in", "traccar", "Input source - can be 'filepath' or 'traccar'") + flag.Parse() + + if showHelp { + flag.Usage() + return + } + + if *serverMode { + run.Server() + } else { + run.CLI(*height, *width, *style, *format, *input) + } +} + +func init() { + log.Msg.Info("Starting map-dot") + fmt.Println("\n" + ascii) + fmt.Println("Creating art from location data") +} + +const ascii string = `███╗ ███╗ █████╗ ██████╗ ██████╗ ██████╗ ████████╗ +████╗ ████║██╔══██╗██╔══██╗██╔══██╗██╔═══██╗╚══██╔══╝ +██╔████╔██║███████║██████╔╝██║ ██║██║ ██║ ██║ +██║╚██╔╝██║██╔══██║██╔═══╝ ██║ ██║██║ ██║ ██║ +██║ ╚═╝ ██║██║ ██║██║ ██████╔╝╚██████╔╝ ██║ +╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ` + +func customUsage() { + extendedHelp := ` +map-dot - Transform location data into artistic heat-map style images + +Usage: + map-dot [--server] [--height=HEIGHT] [--width=WIDTH] [--style=STYLE] [--format=FORMAT] [--input=INPUT] [--id=ID] + +Options: + --server Run in server mode + --height=HEIGHT Output image height + --width=WIDTH Output image width + --style=STYLE Output image style + --format=FORMAT Output image format + --input=INPUT Input source + --id=ID Traccar device ID + +More detailed help information: + -- server (Omit to run in CLI Mode): + Runs a web API on localhost:8198 + Note that there is no authentication built in and the service could expose + personal location data if access is allowed from the internet. + + -- height (Only in CLI Mode): DEFAULT: 1080 + The height of the output image in pixels (Max: 7680) + + -- width (Only in CLI Mode): DEFAULT: 1920 + The width of the output image in pixels (Max: 4320) + + -- style (Only in CLI Mode): DEFAULT: circles + The style of the output image - currently only 'circles' is available + + -- format (Only in CLI Mode): DEFAULT: png + The image format of the output image. Options are: + png, jpeg, gif, bmp, tiff, webp + + -- input (Only in CLI Mode): DEFAULT: traccar + The input source for data. Options are: + traccar, a valid file path in a supported format (See below for formats) + + -- id (Only in CLI Mode): REQUIRED for CLI in 'traccar' mode + The Traccar device ID to fetch data for + +If you want to fetch data from Traccar, you must ensure the following environment variables are set +This applies to Server and CLI modes: + TRACCAR_USER : Traccar Username + TRACCAR_PASS : Traccar Password + TRACCAR_URL : Traccar URL (Including port if not 80/443) + +Input Formats: + Supported input file formats are: + xml+gpx, xml+kml, traccar-Api-JSON + +For Web API usage information start the server and go to localhost:8198/help +` + + fmt.Fprintf(os.Stderr, "%s\n", extendedHelp) +} diff --git a/map-dot b/map-dot new file mode 100755 index 0000000..5b4e90d Binary files /dev/null and b/map-dot differ diff --git a/run/cli.go b/run/cli.go new file mode 100644 index 0000000..c7ffa22 --- /dev/null +++ b/run/cli.go @@ -0,0 +1,19 @@ +package run + +import "fmt" + +func CLI(height, width uint64, style, format, input string) { + fmt.Printf("Running CLI mode with height=%d, width=%d, type=%s, input=%s\n", height, width, style, input) + fmt.Println("CLI Mode not implemented") + + if input == "traccar" { + envCheck() + // Use traccar package to fetch data + // Pass fetched data to imageing package + } + + // Check that `input` is a valid filepath and points to a valid file. + // Use relevent package to parse file + // Pass parsed data to imaging package + fmt.Println("End of implementation") +} diff --git a/run/common.go b/run/common.go new file mode 100644 index 0000000..f333064 --- /dev/null +++ b/run/common.go @@ -0,0 +1,20 @@ +package run + +import ( + "fmt" + "os" + + "git.fjla.uk/fred.boniface/map-dot/log" +) + +func envCheck() { + log.Msg.Debug("reading Traccar credentials from environment") + user := os.Getenv("TRACCAR_USER") + pass := os.Getenv("TRACCAR_PASS") + + if user == "" || pass == "" { + fmt.Println("To use Traccar, you must set the environment variables:\n'TRACCAR_USER', 'TRACCAR_PASS', and for CLI use only 'TRACCAR_DEVID'") + os.Stdout.Sync() // Flush the output buffer + log.Msg.Fatal("Unable to read values from environment") + } +} diff --git a/run/server.go b/run/server.go new file mode 100644 index 0000000..24de6a3 --- /dev/null +++ b/run/server.go @@ -0,0 +1,146 @@ +package run + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "time" +) + +func Server() { + fmt.Println("Server Mode Not Implemented") + envCheck() + + http.HandleFunc("/traccar/", handleTraccarRequest) + http.HandleFunc("/help/", handleHelpRequest) + + serverAddr := "localhost:8198" // Set your desired server address + fmt.Printf("Server listening on http://%s\n", serverAddr) + err := http.ListenAndServe(serverAddr, nil) + if err != nil { + fmt.Printf("Error starting server: %s\n", err) + } +} + +func handleTraccarRequest(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + queryValues := r.URL.Query() + + id, from, to, height, width, style, format, err := validateAndProcessParams(queryValues) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + fmt.Println(id, from, to, height, width, style, format) + + message := map[string]string{ + "status": "success", + "message": "Hello from map-dot", + } + + // Marshal the JSON data + jsonData, err := json.Marshal(message) + if err != nil { + http.Error(w, "JSON encoding error", http.StatusInternalServerError) + return + } + + // Write the JSON response + w.Write(jsonData) +} + +func validateAndProcessParams(queryValues url.Values) (string, string, string, int, int, string, string, error) { + // Validate and process individual parameters + id := queryValues.Get("id") + if id == "" { + return "", "", "", 0, 0, "", "", errors.New("missing required parameter 'id'") + } + + from := queryValues.Get("from") + to := queryValues.Get("to") + heightStr := queryValues.Get("height") + widthStr := queryValues.Get("width") + style := queryValues.Get("style") + format := queryValues.Get("format") + + // Apply defaults if parameters are not specified + if from == "" { + thirtyDaysAgo := time.Now().AddDate(0, 0, -30) + from = thirtyDaysAgo.UTC().Format(time.RFC3339) + } + if to == "" { + to = time.Now().UTC().Format(time.RFC3339) + } + if heightStr == "" { + heightStr = "300" + } + if widthStr == "" { + widthStr = "400" + } + if style == "" { + style = "circle" + } + if format == "" { + format = "png" + } + + // VALIDATE HEIGHT/WIDTH + // Convert height and width to integers + height, errHeight := strconv.Atoi(heightStr) + width, errWidth := strconv.Atoi(widthStr) + if errHeight != nil || errWidth != nil { + return "", "", "", 0, 0, "", "", errors.New("invalid height or width") + } + if height >= 7680 { + return "", "", "", 0, 0, "", "", errors.New("invalid height, max: 7680") + } + if width >= 4320 { + return "", "", "", 0, 0, "", "", errors.New("invalid width, max: 4320") + } + + // VALIDATE FROM/TO + // Parse the ISO date strings to time.Time objects + fromTime, errFrom := time.Parse(time.RFC3339, from) + toTime, errTo := time.Parse(time.RFC3339, to) + if errFrom != nil || errTo != nil { + return "", "", "", 0, 0, "", "", errors.New("invalid date format") + } + + // Define the maximum allowable time duration (e.g., 90 days) + maxAllowableDuration := time.Hour * 24 * 90 + + // Calculate the duration between fromTime and toTime + duration := toTime.Sub(fromTime) + if duration > maxAllowableDuration { + return "", "", "", 0, 0, "", "", errors.New("date range is too wide, max: 90d") + } + // ... Validate other parameters as needed + + return id, from, to, height, width, style, format, nil +} + +func handleHelpRequest(w http.ResponseWriter, r *http.Request) { + helpText := ` +API Usage Information: + +Endpoint: /traccar/:id +Parameters: + - id: Traccar device ID + - from: Start date in ISO format (90-days or less after 'to') + - to: End date in ISO format + - height: Output image height (1-7680) + - width: Output image width (1-4320) + - style: Output image style (circles) + - format: Output image format (png, jpeg, gif, bmp, tiff, webp) + +Example: /traccar/?id=1&from=2023-01-01T00:00:00Z&to=2023-02-01T00:00:00Z&height=600&width=800&style=circle&format=png +` + + w.Header().Set("Content-Type", "text/plain") + fmt.Fprint(w, helpText) +}