Add imageing logic
This commit is contained in:
commit
f98e2d3a7a
7
go.mod
Normal file
7
go.mod
Normal 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
10
go.sum
Normal 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
22
imaging/canvas.go
Normal 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
1
imaging/circles.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package imaging
|
8
imaging/generate.go
Normal file
8
imaging/generate.go
Normal 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
36
log/log.go
Normal 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
108
main.go
Normal 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)
|
||||||
|
}
|
19
run/cli.go
Normal file
19
run/cli.go
Normal 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
20
run/common.go
Normal 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
146
run/server.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user