Add imageing logic

This commit is contained in:
Fred Boniface 2023-08-11 14:32:44 +01:00
commit f98e2d3a7a
11 changed files with 377 additions and 0 deletions

7
go.mod Normal file
View File

@ -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

10
go.sum Normal file
View File

@ -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=

22
imaging/canvas.go Normal file
View File

@ -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)
}
}
}

1
imaging/circles.go Normal file
View File

@ -0,0 +1 @@
package imaging

8
imaging/generate.go Normal file
View File

@ -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
}

36
log/log.go Normal file
View File

@ -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())
}

108
main.go Normal file
View File

@ -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)
}

BIN
map-dot Executable file

Binary file not shown.

19
run/cli.go Normal file
View File

@ -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")
}

20
run/common.go Normal file
View File

@ -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")
}
}

146
run/server.go Normal file
View File

@ -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)
}