change to GORM, add member and rank handlers

This commit is contained in:
2024-03-08 00:39:46 -08:00
parent 84424fdae9
commit 3b715cf331
36 changed files with 3491 additions and 230 deletions

View File

@@ -1,3 +1,3 @@
mariadb.connectionstring=user:password@tcp(localhost:3306)/dbname?parseTime=true
api.prefix=/api/v1
api.port=1323
MARIADB_CONNSTRING=user:password@tcp(localhost:3306)/dbname?parseTime=true
API_PREFIX=/api/v1
API_PORT=1323

110
api/db/award.go Normal file
View File

@@ -0,0 +1,110 @@
package db
/*
DDL
CREATE TABLE `awards` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL,
`short_name` varchar(10) DEFAULT NULL,
`description` text DEFAULT NULL,
`type` varchar(100) DEFAULT NULL,
`footprint` varchar(50) DEFAULT NULL,
`created_at` datetime DEFAULT current_timestamp(),
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`image_url` varchar(250) DEFAULT NULL,
`deleted` tinytext DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=81 DEFAULT CHARSET=utf8mb4 COMMENT='Contains a list of Awards for the unit.';
*/
type Award struct {
ObjectBase
Name string `json:"name"`
ShortName string `json:"short_name"`
Description string `json:"description"`
Type string `json:"type"`
Footprint string `json:"footprint"`
ImageURL string `json:"image_url"`
Members []Member `json:"members" gorm:"many2many:member_awards;"`
}
// Get one by id
func (a *Award) GetByID(id int) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.First(a, id).Error
return err
}
// Get one by name
func (a *Award) GetByName(name string) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Where("name = ?", name).First(a).Error
return err
}
// Get all
func (a *Award) GetAll() ([]Award, error) {
db, err := GetDB()
if err != nil {
return nil, err
}
var awards []Award
err = db.Find(&awards).Error
return awards, err
}
// Create
func (a *Award) Create() error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Create(a).Error
return err
}
// Update
func (a *Award) Update() error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Save(a).Error
return err
}
// Delete
func (a *Award) Delete() error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Delete(a).Error
return err
}
// Get all holders of an award
func (a *Award) GetHolders() ([]Member, error) {
db, err := GetDB()
if err != nil {
return nil, err
}
var members []Member
err = db.Model(a).Association("Members").Find(&members)
return members, err
}

142
api/db/course.go Normal file
View File

@@ -0,0 +1,142 @@
package db
import "context"
/*
DDL
CREATE TABLE `courses` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`short_name` varchar(10) NOT NULL,
`category` varchar(100) NOT NULL,
`description` varchar(1000) DEFAULT NULL,
`image_url` varchar(255) DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`deleted` tinyint(4) DEFAULT 0,
`prereq_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`),
UNIQUE KEY `shortName` (`short_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4;
*/
type Course struct {
ObjectBase
Name string `json:"name"`
ShortName string `json:"short_name"`
Category string `json:"category"`
Description string `json:"description"`
ImageURL string `json:"image_url"`
PassedMembers []Member `json:"passed_members" gorm:"many2many:member_courses_pass;"`
FailedMembers []Member `json:"failed_members" gorm:"many2many:member_courses_fail;"`
Prerequisites []*Course `json:"prerequisites" gorm:"many2many:course_prerequisites;"`
TrainingEvents []TrainingEvent `json:"training_events"`
}
// Get one by id
func (c *Course) GetByID(id int) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.First(c, id).Error
return err
}
// Get one by name
func (c *Course) GetByName(name string) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Where("name = ?", name).First(c).Error
return err
}
// Get all
func (c *Course) GetAll() ([]Course, error) {
db, err := GetDB()
if err != nil {
return nil, err
}
var courses []Course
err = db.Find(&courses).Error
return courses, err
}
// Get all training events for a course
func (c *Course) GetTrainingEvents(ctx context.Context, id int) ([]TrainingEvent, error) {
db, err := GetDB()
if err != nil {
return nil, err
}
var events []TrainingEvent
err = db.WithContext(ctx).Where("course_id = ?", id).Find(&events).Error
if err != nil {
return nil, err
}
return events, nil
}
// Get all prerequisites for a course
func (c *Course) GetPrerequisites(ctx context.Context, id int) ([]Course, error) {
var prereqs []Course
db, err := GetDB()
if err != nil {
return nil, err
}
err = db.WithContext(ctx).Model(c).Where("id = ?", id).Association("Prerequisites").Find(&prereqs)
if err != nil {
return nil, err
}
return prereqs, nil
}
// Create a new course
func (c *Course) Create(ctx context.Context, course Course) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).Create(course).Error
if err != nil {
return err
}
return nil
}
// Update a course
func (c *Course) Update(ctx context.Context, course Course) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).Save(course).Error
if err != nil {
return err
}
return nil
}
// Delete a course
func (c *Course) Delete(ctx context.Context, course Course) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).Delete(course).Error
if err != nil {
return err
}
return nil
}

View File

@@ -3,15 +3,43 @@ package db
import (
"database/sql"
"sync"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/spf13/viper"
"gopkg.in/guregu/null.v3"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var ActiveDB *sql.DB
var ActiveDB *gorm.DB
var activeSQL *sql.DB
var lock = new(sync.Mutex)
func GetDB() (*sql.DB, error) {
type ObjectBase struct {
ID int `json:"id" gorm:"primarykey"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime; not null"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime; not null"`
Deleted bool `json:"deleted" gorm:"default:false; index; not null"`
DeletedAt null.Time `json:"deleted_at"`
}
func AutoMigrate() error {
db, err := GetDB()
if err != nil {
return err
}
err = db.AutoMigrate(
&Award{},
&Course{},
&Member{},
&Rank{},
&TrainingEvent{},
)
return err
}
func GetDB() (*gorm.DB, error) {
if ActiveDB != nil {
return ActiveDB, nil
@@ -26,17 +54,22 @@ func GetDB() (*sql.DB, error) {
return db, nil
}
func ConnectDB() (*sql.DB, error) {
func ConnectDB() (*gorm.DB, error) {
lock.Lock()
defer lock.Unlock()
if ActiveDB != nil {
ActiveDB.Close()
ActiveDB = nil
if activeSQL != nil {
activeSQL.Close()
activeSQL = nil
}
cfg := viper.GetViper()
db, err := sql.Open("mysql", cfg.GetString("MARIADB_CONNSTRING"))
db, err := gorm.Open(mysql.Open(cfg.GetString("MARIADB_CONNSTRING")), &gorm.Config{
FullSaveAssociations: true,
PrepareStmt: true,
})
activeSQL, err = db.DB()
if err != nil {
return nil, err
}

219
api/db/member.go Normal file
View File

@@ -0,0 +1,219 @@
package db
import (
"context"
"github.com/labstack/echo/v4"
"gopkg.in/guregu/null.v3"
)
/*
DDL
CREATE TABLE `members` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`timezone` varchar(5) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`website` varchar(240) DEFAULT NULL,
`guilded_id` varchar(10) DEFAULT NULL,
`steam_id_64` varchar(17) DEFAULT NULL,
`teamspeak_uid` varchar(32) DEFAULT NULL,
`steam_profile_name` varchar(32) DEFAULT NULL,
`discord_id` varchar(20) DEFAULT NULL,
`discord_username` varchar(32) DEFAULT NULL,
`aliases` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`aliases`)),
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`deleted` tinyint(4) DEFAULT NULL,
`remarks` text DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`),
UNIQUE KEY `steamId64` (`steam_id_64`) USING BTREE,
UNIQUE KEY `discordId` (`discord_id`) USING BTREE,
UNIQUE KEY `guilded_id` (`guilded_id`)
) ENGINE=InnoDB AUTO_INCREMENT=186 DEFAULT CHARSET=utf8mb4;
*/
// Member is a struct that represents a member
type Member struct {
ObjectBase
Name string `json:"name"`
Company null.String `json:"company"`
Timezone null.String `json:"timezone"`
Email null.String `json:"email"`
Website null.String `json:"website"`
GuildedID null.String `json:"guilded_id"`
SteamID64 null.String `json:"steam_id_64"`
TeamspeakUID null.String `json:"teamspeak_uid"`
SteamProfileName null.String `json:"steam_profile_name"`
DiscordID null.String `json:"discord_id"`
DiscordUsername null.String `json:"discord_username"`
Aliases null.String `json:"aliases"`
Remarks null.String `json:"remarks"`
Rank Rank `json:"rank" gorm:"references:ID; foreignkey:RankID"`
RankID int `json:"rank_id"`
Awards []Award `json:"awards" gorm:"many2many:member_awards;"`
}
// Get one by id
func (m *Member) GetByID(ctx context.Context, id int) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).First(m, id).Error
if err != nil {
return err
}
return nil
}
// Get all
func (m *Member) GetAll(ctx context.Context) ([]Member, error) {
var members []Member
db, err := GetDB()
if err != nil {
return nil, err
}
// Search by provided struct
err = db.WithContext(ctx).
Model(m).Where(m).Find(&members).Error
if err != nil {
return nil, err
}
return members, nil
}
// Create a new member
func (m *Member) Create(ctx context.Context, c echo.Context) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).Create(m).Error
if err != nil {
return err
}
return nil
}
// Update a member
func (m *Member) Update(ctx context.Context, c echo.Context) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).Save(m).Error
if err != nil {
return err
}
return nil
}
// Delete a member
func (m *Member) Delete(ctx context.Context, c echo.Context) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).Delete(m).Error
if err != nil {
return err
}
return nil
}
// Get by name
func (m *Member) GetByName(ctx context.Context, name string) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).Where("name = ?", name).First(m).Error
if err != nil {
return err
}
return nil
}
// Get by steam id
func (m *Member) GetBySteamID(ctx context.Context, steamID string) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).Where("steam_id_64 = ?", steamID).First(m).Error
if err != nil {
return err
}
return nil
}
// Get by discord id
func (m *Member) GetByDiscordID(ctx context.Context, discordID string) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).Where("discord_id = ?", discordID).First(m).Error
if err != nil {
return err
}
return nil
}
// Get by guilded id
func (m *Member) GetByGuildedID(ctx context.Context, guildedID string) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).Where("guilded_id = ?", guildedID).First(m).Error
if err != nil {
return err
}
return nil
}
// Get by teamspeak uid
func (m *Member) GetByTeamspeakUID(ctx context.Context, teamspeakUID string) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).Where("teamspeak_uid = ?", teamspeakUID).First(m).Error
if err != nil {
return err
}
return nil
}
// Get by email
func (m *Member) GetByEmail(ctx context.Context, email string) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.WithContext(ctx).Where("email = ?", email).First(m).Error
if err != nil {
return err
}
return nil
}
// Get awards a member has
func (m *Member) GetAwards(ctx context.Context) ([]Award, error) {
db, err := GetDB()
if err != nil {
return nil, err
}
err = db.WithContext(ctx).Model(m).Association("Awards").Find(&m.Awards)
if err != nil {
return nil, err
}
return m.Awards, nil
}

109
api/db/rank.go Normal file
View File

@@ -0,0 +1,109 @@
package db
/*
DDL
CREATE TABLE `ranks` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`short_name` varchar(70) NOT NULL,
`category` varchar(100) NOT NULL,
`sort_id` int(11) NOT NULL DEFAULT 0,
`image_url` varchar(240) DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`deleted` tinyint(4) DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`),
UNIQUE KEY `shortName` (`short_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4;
*/
type Rank struct {
ObjectBase
Name string `json:"name"`
ShortName string `json:"short_name"`
Category string `json:"category"`
SortID int `json:"sort_id"`
ImageURL string `json:"image_url"`
Members []Member `json:"members" gorm:"references:ID; foreignkey:RankID"`
}
// Get one by id
func (r *Rank) GetByID(id int) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.First(r, id).Error
return err
}
// Get one by name
func (r *Rank) GetByName(name string) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Where("name = ?", name).First(r).Error
return err
}
// Get all
func (r *Rank) GetAll() ([]Rank, error) {
db, err := GetDB()
if err != nil {
return nil, err
}
var ranks []Rank
err = db.Find(&ranks).Error
return ranks, err
}
// Add a member to a rank
func (r *Rank) AddHolder(m *Member) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Model(r).Association("Members").Append(m)
return err
}
// Create
func (r *Rank) Create() error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Create(r).Error
return err
}
// Update
func (r *Rank) Update() error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Save(r).Error
return err
}
// Delete
func (r *Rank) Delete() error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Delete(r).Error
return err
}

113
api/db/trainingEvent.go Normal file
View File

@@ -0,0 +1,113 @@
package db
/*
DDL
CREATE TABLE `course_events` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`course_id` int(11) DEFAULT NULL,
`event_type` int(11) DEFAULT NULL,
`event_date` datetime NOT NULL,
`guilded_event_id` int(11) DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`deleted` tinyint(4) DEFAULT 0,
`report_url` varchar(2048) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fk_course_events_event_type_id` (`event_type`) USING BTREE,
KEY `courseId` (`course_id`) USING BTREE,
CONSTRAINT `fk_coures_events_course_id` FOREIGN KEY (`course_id`) REFERENCES `courses` (`id`) ON UPDATE CASCADE,
CONSTRAINT `fk_course_events_event_type_id` FOREIGN KEY (`event_type`) REFERENCES `event_types` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
*/
type TrainingEvent struct {
ObjectBase
EventType int `json:"event_type"`
EventDate string `json:"event_date"`
GuildedEventID int `json:"guilded_event_id"`
ReportURL string `json:"report_url"`
Course Course `json:"course" gorm:"references:ID; foreignkey:CourseID"`
CourseID int `json:"course_id"`
}
// Get one by id
func (te *TrainingEvent) GetByID(id int) error {
db, err := GetDB()
if err != nil {
return err
}
err = db.First(te, id).Error
return err
}
// Get all
func (te *TrainingEvent) GetAll() ([]TrainingEvent, error) {
db, err := GetDB()
if err != nil {
return nil, err
}
var tes []TrainingEvent
err = db.Find(&tes).Error
return tes, err
}
// Get all by course
func (te *TrainingEvent) GetAllByCourse(courseID int) ([]TrainingEvent, error) {
db, err := GetDB()
if err != nil {
return nil, err
}
var tes []TrainingEvent
err = db.Where("course_id = ?", courseID).Find(&tes).Error
return tes, err
}
// Create
func (te *TrainingEvent) Create() error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Create(te).Error
return err
}
// Update
func (te *TrainingEvent) Update() error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Save(te).Error
return err
}
// Delete
func (te *TrainingEvent) Delete() error {
db, err := GetDB()
if err != nil {
return err
}
err = db.Delete(te).Error
return err
}
// Get all by date range
func (te *TrainingEvent) GetByDateRange(start string, end string) ([]TrainingEvent, error) {
db, err := GetDB()
if err != nil {
return nil, err
}
var tes []TrainingEvent
err = db.Where("event_date BETWEEN ? AND ?", start, end).Find(&tes).Error
return tes, err
}

View File

@@ -13,6 +13,8 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
@@ -40,4 +42,6 @@ require (
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.5.4 // indirect
gorm.io/gorm v1.25.7 // indirect
)

View File

@@ -7,6 +7,7 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
@@ -16,6 +17,10 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -103,3 +108,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.4 h1:igQmHfKcbaTVyAIHNhhB888vvxh8EdQ2uSUT0LPcBso=
gorm.io/driver/mysql v1.5.4/go.mod h1:9rYxJph/u9SWkWc9yY4XJ1F/+xO0S/ChOmbk3+Z5Tvs=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=

View File

@@ -51,6 +51,13 @@ func main() {
logger.Log.Info().Msg("Connected to the database")
}
err = db.AutoMigrate()
if err != nil {
logger.Log.Fatal().Err(err).Msg("Error migrating the database")
} else {
logger.Log.Info().Msg("Database migrated")
}
// Middleware
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogLatency: true,

View File

@@ -1,198 +0,0 @@
package ops
import (
"context"
"fmt"
"time"
"gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/17th-UnitTracker-API/db"
"github.com/labstack/echo/v4"
"gopkg.in/guregu/null.v3"
)
/*
DDL
CREATE TABLE `members` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`timezone` varchar(5) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`website` varchar(240) DEFAULT NULL,
`guilded_id` varchar(10) DEFAULT NULL,
`steam_id_64` varchar(17) DEFAULT NULL,
`teamspeak_uid` varchar(32) DEFAULT NULL,
`steam_profile_name` varchar(32) DEFAULT NULL,
`discord_id` varchar(20) DEFAULT NULL,
`discord_username` varchar(32) DEFAULT NULL,
`aliases` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`aliases`)),
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`deleted` tinyint(4) DEFAULT NULL,
`remarks` text DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`),
UNIQUE KEY `steamId64` (`steam_id_64`) USING BTREE,
UNIQUE KEY `discordId` (`discord_id`) USING BTREE,
UNIQUE KEY `guilded_id` (`guilded_id`)
) ENGINE=InnoDB AUTO_INCREMENT=186 DEFAULT CHARSET=utf8mb4;
*/
// Member is a struct that represents a member
type Member struct {
ID int `json:"id"`
Name string `json:"name"`
Timezone null.String `json:"timezone"`
Email null.String `json:"email"`
Website null.String `json:"website"`
GuildedID null.String `json:"guilded_id"`
SteamID64 null.String `json:"steam_id_64"`
TeamspeakUID null.String `json:"teamspeak_uid"`
SteamProfileName null.String `json:"steam_profile_name"`
DiscordID null.String `json:"discord_id"`
DiscordUsername null.String `json:"discord_username"`
Aliases null.String `json:"aliases"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Deleted null.Int `json:"deleted"`
Remarks null.String `json:"remarks"`
}
/*
DDL
CREATE TABLE `members` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`timezone` varchar(5) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`website` varchar(240) DEFAULT NULL,
`guilded_id` varchar(10) DEFAULT NULL,
`steam_id_64` varchar(17) DEFAULT NULL,
`teamspeak_uid` varchar(32) DEFAULT NULL,
`steam_profile_name` varchar(32) DEFAULT NULL,
`discord_id` varchar(20) DEFAULT NULL,
`discord_username` varchar(32) DEFAULT NULL,
`aliases` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL CHECK (json_valid(`aliases`)),
`created_at` datetime NOT NULL DEFAULT current_timestamp(),
`updated_at` datetime NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`deleted` tinyint(4) DEFAULT NULL,
`remarks` text DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`),
UNIQUE KEY `steamId64` (`steam_id_64`) USING BTREE,
UNIQUE KEY `discordId` (`discord_id`) USING BTREE,
UNIQUE KEY `guilded_id` (`guilded_id`)
) ENGINE=InnoDB AUTO_INCREMENT=186 DEFAULT CHARSET=utf8mb4;
*/
// GetMembers returns a list of all members
func GetMembers(c echo.Context) error {
members := []Member{}
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
rows, err := db.ActiveDB.QueryContext(
ctx,
"SELECT * FROM members",
)
if err != nil {
return c.JSON(500, map[string]interface{}{
"error": err.Error(),
})
}
if ctx.Err() == context.DeadlineExceeded {
return c.JSON(500, map[string]interface{}{
"error": "request timed out",
})
}
// scan the rows into the members slice
for rows.Next() {
var m Member
err = rows.Scan(
&m.ID,
&m.Name,
&m.Timezone,
&m.Email,
&m.Website,
&m.GuildedID,
&m.SteamID64,
&m.TeamspeakUID,
&m.SteamProfileName,
&m.DiscordID,
&m.DiscordUsername,
&m.Aliases,
&m.CreatedAt,
&m.UpdatedAt,
&m.Deleted,
&m.Remarks,
)
if err != nil {
return c.JSON(500, map[string]interface{}{
"error": err.Error(),
})
}
members = append(members, m)
}
return c.JSON(200, members)
}
// GetMember returns a single member by ID
func GetMember(c echo.Context) error {
id := c.Param("id")
if id == "" {
return fmt.Errorf("id is required")
}
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
returnMember := Member{}
err := db.ActiveDB.QueryRowContext(
ctx,
"SELECT * FROM members WHERE id = ?",
id,
).Scan(
&returnMember.ID,
&returnMember.Name,
&returnMember.Timezone,
&returnMember.Email,
&returnMember.Website,
&returnMember.GuildedID,
&returnMember.SteamID64,
&returnMember.TeamspeakUID,
&returnMember.SteamProfileName,
&returnMember.DiscordID,
&returnMember.DiscordUsername,
&returnMember.Aliases,
&returnMember.CreatedAt,
&returnMember.UpdatedAt,
&returnMember.Deleted,
&returnMember.Remarks,
)
if err != nil {
return c.JSON(500, map[string]interface{}{
"error": err.Error(),
})
}
if ctx.Err() == context.DeadlineExceeded {
return c.JSON(500, map[string]interface{}{
"error": "request timed out",
})
}
return c.JSON(200, returnMember)
}

View File

@@ -1,9 +1,6 @@
package routes
import (
"gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/17th-UnitTracker-API/db"
"gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/17th-UnitTracker-API/ops"
"errors"
"strings"
@@ -19,22 +16,8 @@ func SetupRoutes(
cfg := viper.GetViper()
prefixURL := strings.TrimRight(cfg.GetString("API_PREFIX"), "/")
mainPrefix := e.Group(prefixURL, func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// do something before the next handler
// ensure we always have a db connection
_, err := db.GetDB()
if err != nil {
return err
}
return next(c)
}
})
// MEMBER OPERATIONS
g := mainPrefix.Group("/members")
g.GET("", ops.GetMembers)
g.GET("/:id", ops.GetMember)
mainPrefix := e.Group(prefixURL)
setupMemberRoutes(e, mainPrefix)
setupRankRoutes(e, mainPrefix)
}

141
api/routes/member.go Normal file
View File

@@ -0,0 +1,141 @@
package routes
import (
"strconv"
"gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/17th-UnitTracker-API/db"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
)
func setupMemberRoutes(
e *echo.Echo,
mainPrefix *echo.Group,
) {
// MEMBER OPERATIONS
g := mainPrefix.Group("/member")
// Get all
g.GET("", func(c echo.Context) error {
var err error
member := new(db.Member)
// if query params are present, use them to filter
// otherwise, return all
rank := new(db.Rank)
if c.QueryParam("rank") != "" {
queriedID, err := strconv.Atoi(c.QueryParam("rank"))
if err != nil {
return c.JSON(400, err)
}
err = rank.GetByID(queriedID)
if err == gorm.ErrRecordNotFound {
return c.JSON(404, err)
} else if err != nil {
return c.JSON(500, err)
}
}
award := new(db.Award)
if c.QueryParam("award") != "" {
queriedID, err := strconv.Atoi(c.QueryParam("award"))
if err != nil {
return c.JSON(400, err)
}
err = award.GetByID(queriedID)
if err == gorm.ErrRecordNotFound {
return c.JSON(404, err)
} else if err != nil {
return c.JSON(500, err)
}
}
course := new(db.Course)
if c.QueryParam("course") != "" {
queriedID, err := strconv.Atoi(c.QueryParam("course"))
if err != nil {
return c.JSON(400, err)
}
err = course.GetByID(queriedID)
if err == gorm.ErrRecordNotFound {
return c.JSON(404, err)
} else if err != nil {
return c.JSON(500, err)
}
}
members, err := member.GetAll(c.Request().Context())
if err != nil {
return c.JSON(500, err)
}
return c.JSON(200, members)
})
// Get one by id
g.GET("/:id", func(c echo.Context) error {
member := new(db.Member)
searchID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(400, err)
}
err = member.GetByID(c.Request().Context(), searchID)
if err == gorm.ErrRecordNotFound {
return c.JSON(404, err)
} else if err != nil {
return c.JSON(500, err)
}
return c.JSON(200, member)
})
// Create a new member
g.POST("", func(c echo.Context) error {
member := new(db.Member)
err := c.Bind(member)
if err != nil {
return c.JSON(400, err)
}
err = member.Create(c.Request().Context(), c)
if err != nil {
return c.JSON(500, err)
}
return c.JSON(200, member)
})
// Update a member
g.PUT("/:id", func(c echo.Context) error {
member := new(db.Member)
searchID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(400, err)
}
err = member.GetByID(c.Request().Context(), searchID)
if err == gorm.ErrRecordNotFound {
return c.JSON(404, err)
} else if err != nil {
return c.JSON(500, err)
}
err = c.Bind(member)
if err != nil {
return c.JSON(400, err)
}
err = member.Update(c.Request().Context(), c)
if err != nil {
return c.JSON(500, err)
}
return c.JSON(200, member)
})
// Delete a member
g.DELETE("/:id", func(c echo.Context) error {
member := new(db.Member)
searchID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(400, err)
}
err = member.GetByID(c.Request().Context(), searchID)
if err == gorm.ErrRecordNotFound {
return c.JSON(404, err)
} else if err != nil {
return c.JSON(500, err)
}
err = member.Delete(c.Request().Context(), c)
if err != nil {
return c.JSON(500, err)
}
return c.JSON(200, member)
})
}

127
api/routes/rank.go Normal file
View File

@@ -0,0 +1,127 @@
package routes
import (
"strconv"
"gitea.iceberg-gaming.com/17th-Ranger-Battalion-ORG/17th-UnitTracker-API/db"
"github.com/labstack/echo/v4"
"gorm.io/gorm"
)
func setupRankRoutes(
e *echo.Echo,
mainPrefix *echo.Group,
) {
// RANK OPERATIONS
g := mainPrefix.Group("/rank")
// Get all
g.GET("", func(c echo.Context) error {
rank := new(db.Rank)
ranks, err := rank.GetAll()
if err != nil {
return c.JSON(500, err)
}
return c.JSON(200, ranks)
})
// Get one by id
g.GET("/:id", func(c echo.Context) error {
rank := new(db.Rank)
searchID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(400, err)
}
err = rank.GetByID(searchID)
if err == gorm.ErrRecordNotFound {
return c.JSON(404, err)
} else if err != nil {
return c.JSON(500, err)
}
return c.JSON(200, rank)
})
// Create a new rank
g.POST("", func(c echo.Context) error {
rank := new(db.Rank)
err := c.Bind(rank)
if err != nil {
return c.JSON(400, err)
}
err = rank.Create()
if err != nil {
return c.JSON(500, err)
}
return c.JSON(200, rank)
})
// Update a rank
g.PUT("/:id", func(c echo.Context) error {
rank := new(db.Rank)
searchID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(400, err)
}
err = rank.GetByID(searchID)
if err == gorm.ErrRecordNotFound {
return c.JSON(404, err)
} else if err != nil {
return c.JSON(500, err)
}
err = c.Bind(rank)
if err != nil {
return c.JSON(400, err)
}
err = rank.Update()
if err != nil {
return c.JSON(500, err)
}
return c.JSON(200, rank)
})
// Delete a rank
g.DELETE("/:id", func(c echo.Context) error {
rank := new(db.Rank)
searchID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(400, err)
}
err = rank.GetByID(searchID)
if err == gorm.ErrRecordNotFound {
return c.JSON(404, err)
} else if err != nil {
return c.JSON(500, err)
}
err = rank.Delete()
if err != nil {
return c.JSON(500, err)
}
return c.JSON(200, rank)
})
// Add a member to a rank
g.POST("/:id/member/:memberID", func(c echo.Context) error {
rank := new(db.Rank)
searchID, err := strconv.Atoi(c.Param("id"))
if err != nil {
return c.JSON(400, err)
}
err = rank.GetByID(searchID)
if err == gorm.ErrRecordNotFound {
return c.JSON(404, err)
} else if err != nil {
return c.JSON(500, err)
}
member := new(db.Member)
searchMemberID, err := strconv.Atoi(c.Param("memberID"))
if err != nil {
return c.JSON(400, err)
}
err = member.GetByID(c.Request().Context(), searchMemberID)
if err == gorm.ErrRecordNotFound {
return c.JSON(404, err)
} else if err != nil {
return c.JSON(500, err)
}
err = rank.AddHolder(member)
if err != nil {
return c.JSON(500, err)
}
return c.JSON(200, rank)
})
}

View File

@@ -1,3 +1,3 @@
go 1.22.0
go 1.22
use ./api

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
src/css/images/layers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 600 60"
height="60"
width="600"
id="svg4225"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="spritesheet.svg"
inkscape:export-filename="/home/fpuga/development/upstream/icarto.Leaflet.draw/src/images/spritesheet-2x.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<metadata
id="metadata4258">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4256" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1056"
id="namedview4254"
showgrid="false"
inkscape:zoom="1.3101852"
inkscape:cx="237.56928"
inkscape:cy="7.2419621"
inkscape:window-x="1920"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="svg4225" />
<g
id="enabled"
style="fill:#464646;fill-opacity:1">
<g
id="polyline"
style="fill:#464646;fill-opacity:1">
<path
d="m 18,36 0,6 6,0 0,-6 -6,0 z m 4,4 -2,0 0,-2 2,0 0,2 z"
id="path4229"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 36,18 0,6 6,0 0,-6 -6,0 z m 4,4 -2,0 0,-2 2,0 0,2 z"
id="path4231"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 23.142,39.145 -2.285,-2.29 16,-15.998 2.285,2.285 z"
id="path4233"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
</g>
<path
id="polygon"
d="M 100,24.565 97.904,39.395 83.07,42 76,28.773 86.463,18 Z"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
id="rectangle"
d="m 140,20 20,0 0,20 -20,0 z"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
id="circle"
d="m 221,30 c 0,6.078 -4.926,11 -11,11 -6.074,0 -11,-4.922 -11,-11 0,-6.074 4.926,-11 11,-11 6.074,0 11,4.926 11,11 z"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
id="marker"
d="m 270,19 c -4.971,0 -9,4.029 -9,9 0,4.971 5.001,12 9,14 4.001,-2 9,-9.029 9,-14 0,-4.971 -4.029,-9 -9,-9 z m 0,12.5 c -2.484,0 -4.5,-2.014 -4.5,-4.5 0,-2.484 2.016,-4.5 4.5,-4.5 2.485,0 4.5,2.016 4.5,4.5 0,2.486 -2.015,4.5 -4.5,4.5 z"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<g
id="edit"
style="fill:#464646;fill-opacity:1">
<path
d="m 337,30.156 0,0.407 0,5.604 c 0,1.658 -1.344,3 -3,3 l -10,0 c -1.655,0 -3,-1.342 -3,-3 l 0,-10 c 0,-1.657 1.345,-3 3,-3 l 6.345,0 3.19,-3.17 -9.535,0 c -3.313,0 -6,2.687 -6,6 l 0,10 c 0,3.313 2.687,6 6,6 l 10,0 c 3.314,0 6,-2.687 6,-6 l 0,-8.809 -3,2.968"
id="path4240"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 338.72,24.637 -8.892,8.892 -2.828,0 0,-2.829 8.89,-8.89 z"
id="path4242"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 338.697,17.826 4,0 0,4 -4,0 z"
transform="matrix(-0.70698336,-0.70723018,0.70723018,-0.70698336,567.55917,274.78273)"
id="path4244"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
</g>
<g
id="remove"
style="fill:#464646;fill-opacity:1">
<path
d="m 381,42 18,0 0,-18 -18,0 0,18 z m 14,-16 2,0 0,14 -2,0 0,-14 z m -4,0 2,0 0,14 -2,0 0,-14 z m -4,0 2,0 0,14 -2,0 0,-14 z m -4,0 2,0 0,14 -2,0 0,-14 z"
id="path4247"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
<path
d="m 395,20 0,-4 -10,0 0,4 -6,0 0,2 22,0 0,-2 -6,0 z m -2,0 -6,0 0,-2 6,0 0,2 z"
id="path4249"
inkscape:connector-curvature="0"
style="fill:#464646;fill-opacity:1" />
</g>
</g>
<g
id="disabled"
transform="translate(120,0)"
style="fill:#bbbbbb">
<use
xlink:href="#edit"
id="edit-disabled"
x="0"
y="0"
width="100%"
height="100%" />
<use
xlink:href="#remove"
id="remove-disabled"
x="0"
y="0"
width="100%"
height="100%" />
</g>
<path
style="fill:none;stroke:#464646;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle-3"
d="m 581.65725,30 c 0,6.078 -4.926,11 -11,11 -6.074,0 -11,-4.922 -11,-11 0,-6.074 4.926,-11 11,-11 6.074,0 11,4.926 11,11 z"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

6
src/css/input.css Normal file
View File

@@ -0,0 +1,6 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import 'leaflet.draw.css';
@import 'mapUtils.css';

10
src/css/leaflet.draw.css vendored Normal file
View File

@@ -0,0 +1,10 @@
.leaflet-draw-section{position:relative}.leaflet-draw-toolbar{margin-top:12px}.leaflet-draw-toolbar-top{margin-top:0}.leaflet-draw-toolbar-notop a:first-child{border-top-right-radius:0}.leaflet-draw-toolbar-nobottom a:last-child{border-bottom-right-radius:0}.leaflet-draw-toolbar a{background-image:url('images/spritesheet.png');background-image:linear-gradient(transparent,transparent),url('images/spritesheet.svg');background-repeat:no-repeat;background-size:300px 30px;background-clip:padding-box}.leaflet-retina .leaflet-draw-toolbar a{background-image:url('images/spritesheet-2x.png');background-image:linear-gradient(transparent,transparent),url('images/spritesheet.svg')}
.leaflet-draw a{display:block;text-align:center;text-decoration:none}.leaflet-draw a .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.leaflet-draw-actions{display:none;list-style:none;margin:0;padding:0;position:absolute;left:26px;top:0;white-space:nowrap}.leaflet-touch .leaflet-draw-actions{left:32px}.leaflet-right .leaflet-draw-actions{right:26px;left:auto}.leaflet-touch .leaflet-right .leaflet-draw-actions{right:32px;left:auto}.leaflet-draw-actions li{display:inline-block}
.leaflet-draw-actions li:first-child a{border-left:0}.leaflet-draw-actions li:last-child a{-webkit-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0}.leaflet-right .leaflet-draw-actions li:last-child a{-webkit-border-radius:0;border-radius:0}.leaflet-right .leaflet-draw-actions li:first-child a{-webkit-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px}.leaflet-draw-actions a{background-color:#919187;border-left:1px solid #AAA;color:#FFF;font:11px/19px "Helvetica Neue",Arial,Helvetica,sans-serif;line-height:28px;text-decoration:none;padding-left:10px;padding-right:10px;height:28px}
.leaflet-touch .leaflet-draw-actions a{font-size:12px;line-height:30px;height:30px}.leaflet-draw-actions-bottom{margin-top:0}.leaflet-draw-actions-top{margin-top:1px}.leaflet-draw-actions-top a,.leaflet-draw-actions-bottom a{height:27px;line-height:27px}.leaflet-draw-actions a:hover{background-color:#a0a098}.leaflet-draw-actions-top.leaflet-draw-actions-bottom a{height:26px;line-height:26px}.leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:-2px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polyline{background-position:0 -1px}
.leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-31px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-polygon{background-position:-29px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-62px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-rectangle{background-position:-60px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-92px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circle{background-position:-90px -1px}
.leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-122px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-marker{background-position:-120px -1px}.leaflet-draw-toolbar .leaflet-draw-draw-circlemarker{background-position:-273px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-draw-circlemarker{background-position:-271px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-152px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit{background-position:-150px -1px}
.leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-182px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove{background-position:-180px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-212px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-edit.leaflet-disabled{background-position:-210px -1px}.leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-242px -2px}.leaflet-touch .leaflet-draw-toolbar .leaflet-draw-edit-remove.leaflet-disabled{background-position:-240px -2px}
.leaflet-mouse-marker{background-color:#fff;cursor:crosshair}.leaflet-draw-tooltip{background:#363636;background:rgba(0,0,0,0.5);border:1px solid transparent;-webkit-border-radius:4px;border-radius:4px;color:#fff;font:12px/18px "Helvetica Neue",Arial,Helvetica,sans-serif;margin-left:20px;margin-top:-21px;padding:4px 8px;position:absolute;visibility:hidden;white-space:nowrap;z-index:6}.leaflet-draw-tooltip:before{border-right:6px solid black;border-right-color:rgba(0,0,0,0.5);border-top:6px solid transparent;border-bottom:6px solid transparent;content:"";position:absolute;top:7px;left:-7px}
.leaflet-error-draw-tooltip{background-color:#f2dede;border:1px solid #e6b6bd;color:#b94a48}.leaflet-error-draw-tooltip:before{border-right-color:#e6b6bd}.leaflet-draw-tooltip-single{margin-top:-12px}.leaflet-draw-tooltip-subtext{color:#f8d5e4}.leaflet-draw-guide-dash{font-size:1%;opacity:.6;position:absolute;width:5px;height:5px}.leaflet-edit-marker-selected{background-color:rgba(254,87,161,0.1);border:4px dashed rgba(254,87,161,0.6);-webkit-border-radius:4px;border-radius:4px;box-sizing:content-box}
.leaflet-edit-move{cursor:move}.leaflet-edit-resize{cursor:pointer}.leaflet-oldie .leaflet-draw-toolbar{border:1px solid #999}

117
src/css/mapUtils.css Normal file
View File

@@ -0,0 +1,117 @@
.leaflet-container .leaflet-grid-mouseposition {
background-color: rgba(255, 255, 255, 0.7);
box-shadow: 0 0 5px #bbb;
padding: 5px;
margin: 20px;
color: #000;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
max-width: 300px;
text-align: center;
}
.drawingTooltip {
/* transparent white background */
background-color: rgba(255, 255, 255, 0.5);
box-shadow: none;
padding: 5px;
margin: 0;
border: none;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 12px;
color: #000;
font-weight: 700;
}
#marker-select-container {
width: 300px;
height: 400px;
}
.marker-select {
width: 100%;
height: 100%;
border: 1px solid #ccc;
background-color: gray;
cursor: pointer;
padding: 0;
margin: 8px;
outline: none;
display: flex;
flex-wrap: wrap;
overflow-y: scroll;
overflow-x: hidden;
}
.marker-select-option {
/* make rows with name and preview to the right */
display: flex;
flex-direction: row;
align-items: left;
/* align text in center, vertically */
justify-content: center;
padding: 2px;
width: 100%;
}
.marker-select-image {
margin: 2px;
width: 128px;
max-height: 128px;
border: 1px solid #ccc;
cursor: pointer;
padding: 0;
outline: none;
}
.marker-select-name {
font-size: 12px;
text-align: center;
margin: 0;
padding: 0;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before,
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before {
display: none;
}
.sp-replacer {
margin: 0;
overflow: hidden;
cursor: pointer;
padding: unset !important;
display: inline-block;
border: unset !important;
background: unset !important;
color: #333;
vertical-align: middle;
}
.sp-dd {
display: none;
}
.sp-preview {
margin-right: unset !important;
}
.leaflet-pm-toolbar .leaflet-pm-icon-export {
background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI1LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA1MTIgNTEyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxnIGlkPSJfMSI+Cgk8cGF0aCBkPSJNMTg2LDUwLjFoMTM4LjNjOC4zLDAsMTUsNi43LDE1LDE1VjIwMmg1Ni4yYzguMywwLDE1LDYuNywxNSwxNWMwLDMuNy0xLjQsNy4zLTMuOSwxMC4xTDI2Ni4zLDM4Mi43CgkJYy01LjUsNi4yLTE1LDYuNy0yMS4yLDEuMWMtMC40LTAuMy0wLjgtMC43LTEuMS0xLjFMMTAzLjcsMjI3LjFjLTUuNi02LjEtNS4xLTE1LjYsMS4xLTIxLjJjMi44LTIuNSw2LjMtMy45LDEwLjEtMy45SDE3MVY2NS4xCgkJQzE3MSw1Ni45LDE3Ny43LDUwLjEsMTg2LDUwLjF6IE0zMDkuMyw4MC4xSDIwMVYyMTdjMCw4LjMtNi43LDE1LTE1LDE1bDAsMGgtMzcuNGwxMDYuNiwxMTguMkwzNjEuNywyMzJoLTM3LjUKCQljLTguMywwLTE1LTYuNy0xNS0xNWwwLDBWODAuMXoiLz4KCTxwYXRoIGQ9Ik0zOTAuOCw0NTIuMUgxMTkuNGMtMzYsMC02NS4zLTI5LjMtNjUuMy02NS4zdi0zOC4yYzAtOC4zLDYuNy0xNSwxNS0xNXMxNSw2LjcsMTUsMTV2MzguMmMwLDE5LjUsMTUuOCwzNS4zLDM1LjMsMzUuMwoJCWgyNzEuNGMxOS41LDAsMzUuMy0xNS44LDM1LjMtMzUuM3YtMzguMmMwLTguMyw2LjctMTUsMTUtMTVzMTUsNi43LDE1LDE1djM4LjJDNDU2LjEsNDIyLjksNDI2LjksNDUyLjEsMzkwLjgsNDUyLjF6Ii8+CjwvZz4KPC9zdmc+Cg==")
}
.leaflet-pm-toolbar .leaflet-pm-icon-import {
background-image: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pjxzdmcgdmlld0JveD0iMCAwIDUxMiA1MTIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHRpdGxlLz48ZyBkYXRhLW5hbWU9IjEiIGlkPSJfMSI+PHBhdGggZD0iTTMyNC4zLDM4Ny42OUgxODZhMTUsMTUsMCwwLDEtMTUtMTVWMjM1LjhIMTE0LjgxYTE1LDE1LDAsMCwxLTExLjE0LTI1LjA1TDI0NCw1NS4xYTE1LDE1LDAsMCwxLDIyLjI5LDBMNDA2LjYsMjEwLjc1YTE1LDE1LDAsMCwxLTExLjE0LDI1LjA1SDMzOS4zVjM3Mi42OUExNSwxNSwwLDAsMSwzMjQuMywzODcuNjlaTTIwMSwzNTcuNjlIMzA5LjNWMjIwLjhhMTUsMTUsMCwwLDEsMTUtMTVoMzcuNDRMMjU1LjEzLDg3LjU1LDE0OC41MywyMDUuOEgxODZhMTUsMTUsMCwwLDEsMTUsMTVaIi8+PHBhdGggZD0iTTM5MC44NCw0NTIuMTVIMTE5LjQzYTY1LjM3LDY1LjM3LDAsMCwxLTY1LjMtNjUuM1YzNDguNjhhMTUsMTUsMCwwLDEsMzAsMHYzOC4xN2EzNS4zNCwzNS4zNCwwLDAsMCwzNS4zLDM1LjNIMzkwLjg0YTM1LjMzLDM1LjMzLDAsMCwwLDM1LjI5LTM1LjNWMzQ4LjY4YTE1LDE1LDAsMSwxLDMwLDB2MzguMTdBNjUuMzcsNjUuMzcsMCwwLDEsMzkwLjg0LDQ1Mi4xNVoiLz48L2c+PC9zdmc+");
}
.leaflet-pm-toolbar .leaflet-pm-icon-logout {
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBVcGxvYWRlZCB0bzogU1ZHIFJlcG8sIHd3dy5zdmdyZXBvLmNvbSwgR2VuZXJhdG9yOiBTVkcgUmVwbyBNaXhlciBUb29scyAtLT4NCjxzdmcgZmlsbD0iIzAwMDAwMCIgaGVpZ2h0PSI4MDBweCIgd2lkdGg9IjgwMHB4IiB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIA0KCSB2aWV3Qm94PSIwIDAgMzg0Ljk3MSAzODQuOTcxIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxnPg0KCTxnIGlkPSJTaWduX091dCI+DQoJCTxwYXRoIGQ9Ik0xODAuNDU1LDM2MC45MUgyNC4wNjFWMjQuMDYxaDE1Ni4zOTRjNi42NDEsMCwxMi4wMy01LjM5LDEyLjAzLTEyLjAzcy01LjM5LTEyLjAzLTEyLjAzLTEyLjAzSDEyLjAzDQoJCQlDNS4zOSwwLjAwMSwwLDUuMzksMCwxMi4wMzFWMzcyLjk0YzAsNi42NDEsNS4zOSwxMi4wMywxMi4wMywxMi4wM2gxNjguNDI0YzYuNjQxLDAsMTIuMDMtNS4zOSwxMi4wMy0xMi4wMw0KCQkJQzE5Mi40ODUsMzY2LjI5OSwxODcuMDk1LDM2MC45MSwxODAuNDU1LDM2MC45MXoiLz4NCgkJPHBhdGggZD0iTTM4MS40ODEsMTg0LjA4OGwtODMuMDA5LTg0LjJjLTQuNzA0LTQuNzUyLTEyLjMxOS00Ljc0LTE3LjAxMSwwYy00LjcwNCw0Ljc0LTQuNzA0LDEyLjQzOSwwLDE3LjE3OWw2Mi41NTgsNjMuNDZIOTYuMjc5DQoJCQljLTYuNjQxLDAtMTIuMDMsNS40MzgtMTIuMDMsMTIuMTUxYzAsNi43MTMsNS4zOSwxMi4xNTEsMTIuMDMsMTIuMTUxaDI0Ny43NGwtNjIuNTU4LDYzLjQ2Yy00LjcwNCw0Ljc1Mi00LjcwNCwxMi40MzksMCwxNy4xNzkNCgkJCWM0LjcwNCw0Ljc1MiwxMi4zMTksNC43NTIsMTcuMDExLDBsODIuOTk3LTg0LjJDMzg2LjExMywxOTYuNTg4LDM4Ni4xNjEsMTg4Ljc1NiwzODEuNDgxLDE4NC4wODh6Ii8+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KCTxnPg0KCTwvZz4NCgk8Zz4NCgk8L2c+DQoJPGc+DQoJPC9nPg0KPC9nPg0KPC9zdmc+');
}

34
src/draw.html Normal file
View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Arma 3 Planner</title>
<link href="index.css" rel="stylesheet">
<script defer src="index.js"></script>
</head>
<body style="margin: 0; padding: 0; border: 0">
<div class="mapContainer" id="mapContainer" style="width: 100%; height: 100vh; margin: 0; padding: 0; border: 0">
</div>
<div id="marker-select-container">
<div id="marker-select"></div>
</div>
<!-- Maplibre GL -->
<link href="https://unpkg.com/maplibre-gl@2.2.1/dist/maplibre-gl.css" rel="stylesheet" />
<script src="https://unpkg.com/maplibre-gl@2.2.1/dist/maplibre-gl.js"></script>
<script src="https://unpkg.com/pmtiles@2.9.0/dist/index.js"></script>
<script src="https://unpkg.com/proj4@2.5.0/dist/proj4.js"></script>
<script src='https://unpkg.com/@turf/turf@6/turf.min.js'></script>
<script src="https://unpkg.com/@maplibre/maplibre-gl-leaflet@0.0.17/leaflet-maplibre-gl.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
getWorldData("gulfcoast");
</script>
</body>
</html>

778
src/index.css Normal file
View File

@@ -0,0 +1,778 @@
/*
! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
[type='button'],
[type='reset'],
[type='submit'] {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
}
.container {
width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1280px;
}
}
@media (min-width: 1536px) {
.container {
max-width: 1536px;
}
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.mb-4 {
margin-bottom: 1rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.mt-8 {
margin-top: 2rem;
}
.block {
display: block;
}
.flex {
display: flex;
}
.grid {
display: grid;
}
.hidden {
display: none;
}
.h-4 {
height: 1rem;
}
.h-screen {
height: 100vh;
}
.w-1\/3 {
width: 33.333333%;
}
.w-full {
width: 100%;
}
.max-w-md {
max-width: 28rem;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.rounded {
border-radius: 0.25rem;
}
.rounded-lg {
border-radius: 0.5rem;
}
.rounded-md {
border-radius: 0.375rem;
}
.border {
border-width: 1px;
}
.border-2 {
border-width: 2px;
}
.border-gray-300 {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity));
}
.bg-blue-500 {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
.bg-gray-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 244 246 / var(--tw-bg-opacity));
}
.bg-green-500 {
--tw-bg-opacity: 1;
background-color: rgb(34 197 94 / var(--tw-bg-opacity));
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.p-2 {
padding: 0.5rem;
}
.p-4 {
padding: 1rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.px-4 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.text-center {
text-align: center;
}
.text-3xl {
font-size: 1.875rem;
line-height: 2.25rem;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
}
.font-bold {
font-weight: 700;
}
.text-gray-500 {
--tw-text-opacity: 1;
color: rgb(107 114 128 / var(--tw-text-opacity));
}
.text-green-500 {
--tw-text-opacity: 1;
color: rgb(34 197 94 / var(--tw-text-opacity));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.text-white {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.shadow-md {
--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}
.filter {
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
}
.hover\:bg-blue-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
}
.hover\:bg-green-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(21 128 61 / var(--tw-bg-opacity));
}

58
src/index.html Normal file
View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<!-- This is a basic page with two centered sections, one to create a new session and one to input an existing session UUID with a button to search and join. -->
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Arma 3 Planner</title>
<link href="index.css" rel="stylesheet">
<script defer src="index.js"></script>
</head>
<body class="bg-gray-100">
<div class="flex justify-center items-center h-screen">
<div class="w-1/3">
<h1 class="text-4xl font-bold text-center mb-8">Arma 3 Planner</h1>
<!-- Spacer -->
<div class="h-4"></div>
<!-- Surround the below with a border -->
<div class="border-2 border-gray-300 p-4 rounded mb-4">
<!-- Create session will post credentials and generate session -->
<form action="/create" method="post">
<input type="text" name="username" class="w-full border-2 border-gray-300 p-2 rounded mb-4"
autocomplete="nickname" placeholder="Your Name">
<input type="text" name="sessionName" class="w-full border-2 border-gray-300 p-2 rounded mb-4"
placeholder="Session Name">
<input type="password" name="sessionPassword" class="w-full border-2 border-gray-300 p-2 rounded mb-4"
autocomplete="current-password" placeholder="Session Password">
<button type="submit"
class="w-full bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-4 rounded mb-4">Create New
Session</button>
</form>
<!-- divider -->
<div class="text-center mb-4">
<span class="text-gray-500">or</span>
</div>
<div>
<!-- Submitting form will trigger JS method to send RabbitMQ data -->
<form onsubmit="joinSession(event)" class="mb-4">
<input type="text" name="username" class="w-full border-2 border-gray-300 p-2 rounded mb-4"
autocomplete="nickname" placeholder="Your Name">
<input type="text" name="sessionCode" class="w-full border-2 border-gray-300 p-2 rounded mb-4"
placeholder="Session Code">
<input type="password" name="sessionPassword" class="w-full border-2 border-gray-300 p-2 rounded mb-4"
autocomplete="current-password" placeholder="Session Password">
<button type="submit"
class="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Join
Session</button>
</form>
</div>
</div>
</div>
</div>
</body>

11
src/index.js Normal file
View File

@@ -0,0 +1,11 @@
// import all js
import './src/js/leaflet.draw.js'
import './src/js/defaultMap.js'
import './src/js/mapUtils.js'
import './src/js/sessionMgmt.js'
import * as L from "leaflet";
import "leaflet/dist/leaflet.css";
import "@geoman-io/leaflet-geoman-free";
import "@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css";

552
src/js/defaultMap.js Normal file
View File

@@ -0,0 +1,552 @@
function getWorldData (worldName) {
// Get the metadata of the map
$.getJSON(`https://styles.ocap2.com/${worldName}.json`)
.done(function (styleJson) {
window.worldMeta = styleJson.metadata;
return InitMap(styleJson.metadata);
}).fail(function (jqxhr, textStatus, error) {
var err = textStatus + ', ' + error;
console.error('Request Failed: ' + err);
});
}
function InitMap (worldMeta) {
$(function () {
if (!worldMeta) {
throw new Error('World metadata not found');
}
map = L.map('mapContainer', {
// crs: mapInfos.CRS,
crs: L.CRS.EPSG3857,
maxZoom: 19,
minZoom: 12,
zoom: 12,
zoomControl: true,
center: worldMeta.center,
attributionControl: false, // set up later
});
console.debug('Map initialized', map);
console.debug('World metadata', worldMeta);
// Set initial bounds
var mapBounds = [[
worldMeta.bounds[0],
worldMeta.bounds[1],
],
[
worldMeta.bounds[2],
worldMeta.bounds[3],
]]
console.debug('Map bounds', mapBounds);
map.fitBounds(mapBounds, {
padding: [0, 0],
maxZoom: 19,
animate: false,
})
// Limit map bounds (panning) at 3x the map size
var bbox = turf.bboxPolygon(worldMeta.bounds);
// console.debug('Map bbox', bbox);
var limitBoundsPoly = turf.transformScale(bbox, 3);
// console.debug('Map limitBoundspoly', limitBoundsPoly);
var limitBounds = limitBoundsPoly.geometry.coordinates[0];
// console.debug('Map limitBounds', limitBounds);
map.setMaxBounds(limitBounds);
// ADD TOP RIGHT CONTROLS
// add control in top right with recent sessions
const recentSessionsControl = L.control({
position: 'topright',
})
recentSessionsControl.onAdd = (map) => {
var recentSessions = localStorage.getItem('recentSessions') || '[]';
// If session search param set, store in cache
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const session = urlParams.get('session');
if (session) {
recentSessions = JSON.parse(recentSessions);
if (!recentSessions.includes(session)) {
// if not already listed, insert at beginning of array
recentSessions.unshift(session);
localStorage.setItem('recentSessions', JSON.stringify(recentSessions));
} else {
// move to top if already in array
recentSessions = recentSessions.filter((s) => s !== session);
recentSessions.unshift(session);
localStorage.setItem('recentSessions', JSON.stringify(recentSessions));
}
}
// Use recent missions to populate info window
var recentSessionsHtml = '<div id="recentSessions" style="background-color: white; padding: 10px;"><h3>Recent Sessions</h3><ul>';
for (let session of recentSessions || []) {
if (session === recentSessions[0]) {
// if first, add a "current" label
recentSessionsHtml += '<li>Current: <a href="/draw?session=' + session + '">' + session + '</a></li>';
} else {
// otherwise, add normally
recentSessionsHtml += '<li><a href="/draw?session=' + session + '">' + session + '</a></li>';
}
// skip any past 7
if (session === recentSessions[6]) {
break;
}
}
recentSessionsHtml += '</ul></div>';
var div = L.DomUtil.create('div', 'recentSessions');
div.innerHTML = recentSessionsHtml;
return div;
}
// finish adding the top right controls
recentSessionsControl.addTo(map);
L.control.gridMousePosition().addTo(map);
// ADD BOTTOM LEFT CONTROLS
L.control.scale({
maxWidth: 200,
imperial: false
}).addTo(map);
// ADD BOTTOM RIGHT CONTROLS
// set the Leaflet attribution control
const attributionControl = L.control.attribution({
position: 'bottomright',
});
attributionControl.addAttribution(worldMeta.attribution)
attributionControl.addTo(map);
// ADD OTHER
L.latlngGraticule({
color: '#777',
font: '12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif',
fontColor: '#777',
zoomInterval: [
{ start: 12, end: 13, interval: 10000 },
{ start: 13, end: 16, interval: 1000 },
{ start: 16, end: 20, interval: 100 }
]
}).addTo(map);
// set up maplibre vector basemap
let protocol = new pmtiles.Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile);
window.maplibre = L.maplibreGL({
style: 'https://styles.ocap2.com/' + worldMeta.worldname + '.json',
minZoom: 0,
maxZoom: 24,
}).addTo(map);
if (window.location.hash == '#cities') {
$.each(mapInfos.cities, function (index, city) {
L.marker([city.y, city.x]).addTo(map).bindPopup(city.name);
});
}
// Function to update draw colors based on the selected color
window.DRAW_COLOR = '#3388ff';
function updateDrawColors (color) {
DRAW_COLOR = color;
}
// load w/ content
async function fetchAvailableMarkers () {
return fetch('/markers')
.then((response) => response.json())
.then((data) => {
console.debug('Loaded markers', data)
return data;
});
}
fetchAvailableMarkers().then(async (markers) => {
var markerSelectContainer = document.getElementById('marker-select-container');
// select addon keys of all markers array and dedupe
var addons = [...new Set(markers.addon)];
// Add markers to the marker select
for (let addon of addons) {
// Add a header for the addon
var addonHeader = document.createElement('h3');
addonHeader.innerHTML = addon;
markerSelectContainer.appendChild(addonHeader);
// Add a container for the addon's markers
var markerSelect = document.createElement('div');
markerSelect.className = 'marker-select';
markerSelectContainer.appendChild(markerSelect);
// Add each marker to the marker select
// get all markers where addon matches
for (let marker of markers.all) {
if (marker.addon === addon) {
var markerOption = document.createElement('div');
markerOption.className = 'marker-select-option';
markerOption.innerHTML = `<span class="marker-select-name">${marker.name}</span><img src="${marker.url}" class="marker-select-image" />`;
markerOption.addEventListener('click', function () {
// Set the marker image
var markerImage = document.getElementById('image-url');
markerImage.value = marker.url;
// Set the marker description
var markerDescription = document.getElementById('description');
markerDescription.value = marker.description;
});
markerSelect.appendChild(markerOption);
}
}
}
});
// Function to prompt the user to select a marker
function promptMarkerSelection () {
// open a dialog to select a marker
Swal.fire({
title: 'Select a Marker',
html: document.getElementById('marker-select-container'),
showConfirmButton: false,
showCloseButton: true,
showCancelButton: true,
cancelButtonText: 'Cancel',
cancelButtonColor: '#d33',
onOpen: () => {
// Add a listener to the marker select button
document.getElementById('marker-select-button').addEventListener('click', function () {
Swal.close();
});
}
});
}
// promptMarkerSelection()
// Update the 'pm:create' event listener
map.on('pm:create', function (event) {
console.log(event.shape);
var layer = event.layer;
var imageUrl;
var description;
if (event.shape === 'Marker') {
Swal.fire({
title: 'Set Marker Image and Description',
html: `
<p>Marker URL</p>
<input id="image-url" class="swal2-input" placeholder="Enter the URL of the marker image">
<p>You can also click here to select a marker image</p>
<button id="marker-select-button" class="swal2-input" style="width: 100%; height: 50px; border: 1px solid #ccc; border-radius: 5px; background-color: white; margin-bottom: 10px;" onClick="promptMarkerSelection()">Select Marker</button>
<textarea id="description" rows="4" class="swal2-textarea" placeholder="Enter a description for the drawing"></textarea>
`,
showCancelButton: true,
confirmButtonText: 'Set',
cancelButtonText: 'Cancel',
allowOutsideClick: false,
preConfirm: () => {
imageUrl = document.getElementById('image-url').value;
description = document.getElementById('description').value;
if (!imageUrl || imageUrl == '') {
imageUrl = 'https://i.imgur.com/SY0C1lx.png';
}
var icon = L.icon({
iconUrl: imageUrl,
iconSize: [25, 41] // Adjust the size of the icon if needed
});
layer.setIcon(icon);
if (description.trim() !== '') {
layer.description = description;
layer.bindPopup(description).openPopup();
}
}
}).then((result) => {
// Get the selected color from the color picker
var selectedColor = $('#colorPicker').spectrum('get').toHexString();
// Send the drawing data to the server
saveDrawingToServer(layer, description, selectedColor, imageUrl);
});
} else {
Swal.fire({
title: 'Enter a description for the drawing:',
input: 'textarea',
inputPlaceholder: 'Description',
showCancelButton: true,
confirmButtonText: 'Save',
cancelButtonText: 'Cancel',
allowOutsideClick: false
}).then((result) => {
if (result.isConfirmed) {
description = result.value;
layer.description = description;
layer.bindPopup(description);
// create tooltip
layer.bindTooltip(description, {
permanent: true,
direction: 'bottom',
className: 'drawingTooltip',
});
} else if (result.dismiss === Swal.DismissReason.cancel) {
// Cancelled, do nothing
}
}).then((result) => {
// Get the selected color from the color picker
var selectedColor = $('#colorPicker').spectrum('get').toHexString();
layer.setStyle({ color: selectedColor }); // Set the color of the layer
// Send the drawing data to the server
saveDrawingToServer(layer, description, selectedColor, imageUrl);
});
}
});
// Function to update draw colors based on the selected color
window.DRAW_COLOR = '#3388ff';
function updateDrawColors (color) {
DRAW_COLOR = color;
}
// Function to send the drawing data to the server
function saveDrawingToServer (layer, description, color, imageUrl) {
// get session search param from current url
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const session = urlParams.get('session');
$.ajax({
url: '/drawings/' + session,
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({
data: layer.toGeoJSON(),
description: description,
color: color,
imageUrl: imageUrl, // Include the imageUrl in the request
}),
success: function (response) {
// Get the ID of the saved drawing from the server response
const drawingId = response.id;
// Set the ID as a property of the layer
layer.drawingId = drawingId;
console.log('Drawing saved successfully.');
},
error: function (xhr, status, error) {
console.error('Error saving drawing:', error);
},
});
}
// Event listener for drawing deletion
map.on('pm:remove', function (event) {
console.log('Drawing deleted. ');
var layer = event.layer;
deleteDrawingOnServer(layer);
});
// Function to delete a drawing from the server
function deleteDrawingOnServer (layer) {
console.log('Deleting drawing...');
console.log(layer.drawingId);
const drawingId = layer.drawingId;
if (drawingId) {
$.ajax({
url: `/drawings/${drawingId}`,
type: 'DELETE',
success: function () {
console.log('Drawing deleted successfully.');
},
error: function (xhr, status, error) {
console.error('Error deleting drawing:', error);
},
});
}
}
// Function to export the drawings
function exportDrawings () {
window.location.href = '/export';
}
// Function to import the drawings
function importDrawings () {
var input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = function (event) {
var file = event.target.files[0];
if (file) {
var reader = new FileReader();
reader.onload = function (e) {
var fileData = e.target.result;
importDrawingsFromFile(fileData);
};
reader.readAsText(file);
}
};
input.click();
}
// Function to import the drawings from a file
function importDrawingsFromFile (fileData) {
$.ajax({
url: '/import',
type: 'POST',
contentType: 'application/json',
data: fileData,
success: function () {
console.log('Drawings imported successfully.');
// Reload the page to display the imported drawings
location.reload();
},
error: function (xhr, status, error) {
console.error('Error importing drawings:', error);
},
});
}
// Load drawings from the server
loadDrawingsFromServer();
function loadDrawingsFromServer () {
// get session search param from current url
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
const session = urlParams.get('session');
$.ajax({
url: `/drawings/${session}`,
type: 'GET',
dataType: 'json',
success: function (drawings) {
drawings.forEach(function (drawing) {
var layer;
if (drawing.data.type === 'Point') {
var icon = L.icon({
iconUrl: drawing.imageUrl,
iconSize: [25, 41], // Adjust the size of the icon if needed
});
layer = L.geoJSON(drawing.data, {
pointToLayer: function (geoJsonPoint, latlng) {
return L.marker(latlng, {
icon: icon
});
},
});
} else {
layer = L.geoJSON(drawing.data, {
style: {
color: drawing.color,
},
});
}
layer.eachLayer(function (l) {
var popup = L.popup({
maxWidth: 200,
className: 'drawingPopup',
});
popup.setContent(drawing.description);
l.bindPopup(popup);
// create tooltip
l.bindTooltip(drawing.description, {
permanent: true,
direction: 'bottom',
className: 'drawingTooltip',
});
l.drawingId = drawing.id; // Set the drawing ID as a property of the layer
});
layer.addTo(map);
});
console.log('Drawings loaded successfully.');
},
error: function (xhr, status, error) {
console.error('Error loading drawings:', error);
},
});
checkIt();
}
function checkIt () {
$.ajax({
url: '/loginStatus',
type: 'GET',
dataType: 'json',
success: function (response) {
if (response.isLoggedIn) {
map.pm.addControls({
position: 'topleft',
drawCircle: false,
drawRectangle: true,
drawCircleMarker: false,
tooltips: true,
drawPolyline: true,
drawPolygon: true,
drawText: false,
});
// Add color picker functionality
var colorPicker = $('<input type="text" id="colorPicker" />');
var logout = $('<div class="button-container" title="Logout"><a class="leaflet-buttons-control-button" role="button" tabindex="0" href="/logout"><div class="control-icon leaflet-pm-icon-logout"></div></a></div>');
// Create the import button
var importButton = $('<div class="button-container" title="Import Drawings"><a class="leaflet-buttons-control-button" role="button" tabindex="0" id="importButton"><div class="control-icon leaflet-pm-icon-import"></div></a></div>');
$('.leaflet-pm-toolbar:last').append(importButton);
// Create the export button
var exportButton = $('<div class="button-container" title="Export Drawings"><a class="leaflet-buttons-control-button" role="button" tabindex="0" id="exportButton"><div class="control-icon leaflet-pm-icon-export"></div></a></div>');
$('.leaflet-pm-toolbar:last').append(exportButton);
$('.leaflet-pm-toolbar:last').append(logout);
$('.leaflet-pm-toolbar:first').prepend(colorPicker);
$('#colorPicker').spectrum({
color: '#3388ff', // Initial color
preferredFormat: 'hex',
showInput: true,
change: function (color) {
var selectedColor = color.toHexString();
updateDrawColors(selectedColor);
},
});
// Event listener for export button click
$('#exportButton').on('click', function () {
exportDrawings();
});
// Event listener for import button click
$('#importButton').on('click', function () {
importDrawings();
});
}
},
error: function (xhr, status, error) {
console.error('Error checking login status:', error);
},
});
}
});
}

10
src/js/leaflet.draw.js Normal file

File diff suppressed because one or more lines are too long

628
src/js/mapUtils.js Normal file
View File

@@ -0,0 +1,628 @@
// leaflet.latlng-graticule.js
// https://github.com/cloudybay/leaflet.latlng-graticule
/* eslint-disable indent,semi */
/**
* Create a Canvas as ImageOverlay to draw the Lat/Lon Graticule,
* and show the axis tick label on the edge of the map.
* Author: lanwei@cloudybay.com.tw
*/
(function (window, document, undefined) {
L.LatLngGraticule = L.Layer.extend({
includes: (L.Evented.prototype || L.Mixin.Events),
options: {
showLabel: true,
opacity: 1,
weight: 0.8,
color: '#aaa',
font: '12px Verdana',
dashArray: [0, 0],
lngLineCurved: 0,
latLineCurved: 0,
zoomInterval: [
{ start: 2, end: 2, interval: 40 },
{ start: 3, end: 3, interval: 20 },
{ start: 4, end: 4, interval: 10 },
{ start: 5, end: 7, interval: 5 },
{ start: 8, end: 20, interval: 1 }
],
sides: ['N', 'S', 'E', 'W']
},
initialize: function (options) {
L.setOptions(this, options);
var defaultFontName = 'Verdana';
var _ff = this.options.font.split(' ');
if (_ff.length < 2) {
this.options.font += ' ' + defaultFontName;
}
if (!this.options.fontColor) {
this.options.fontColor = this.options.color;
}
if (this.options.zoomInterval) {
if (this.options.zoomInterval.latitude) {
this.options.latInterval = this.options.zoomInterval.latitude;
if (!this.options.zoomInterval.longitude) {
this.options.lngInterval = this.options.zoomInterval.latitude;
}
}
if (this.options.zoomInterval.longitude) {
this.options.lngInterval = this.options.zoomInterval.longitude;
if (!this.options.zoomInterval.latitude) {
this.options.latInterval = this.options.zoomInterval.longitude;
}
}
if (!this.options.latInterval) {
this.options.latInterval = this.options.zoomInterval;
}
if (!this.options.lngInterval) {
this.options.lngInterval = this.options.zoomInterval;
}
}
},
onAdd: function (map) {
this._map = map;
if (!this._canvas) {
this._initCanvas();
}
map._panes.overlayPane.appendChild(this._canvas);
map.on('viewreset', this._reset, this);
map.on('move', this._reset, this);
map.on('moveend', this._reset, this);
// if (map.options.zoomAnimation && L.Browser.any3d) {
// map.on('zoomanim', this._animateZoom, this);
// }
this._reset();
},
onRemove: function (map) {
L.DomUtil.remove(this._canvas);
map.off('viewreset', this._reset, this);
map.off('move', this._reset, this);
map.off('moveend', this._reset, this);
if (map.options.zoomAnimation) {
map.off('zoomanim', this._animateZoom, this);
}
},
addTo: function (map) {
map.addLayer(this);
return this;
},
setOpacity: function (opacity) {
this.options.opacity = opacity;
this._updateOpacity();
return this;
},
bringToFront: function () {
if (this._canvas) {
//this._map._panes.overlayPane.appendChild(this._canvas);
}
return this;
},
bringToBack: function () {
var pane = this._map._panes.overlayPane;
if (this._canvas) {
//pane.insertBefore(this._canvas, pane.firstChild);
}
return this;
},
getAttribution: function () {
return this.options.attribution;
},
_initCanvas: function () {
this._canvas = L.DomUtil.create('canvas', '');
if (this._map.options.zoomAnimation && L.Browser.any3d) {
L.DomUtil.addClass(this._canvas, 'leaflet-zoom-animated');
} else {
L.DomUtil.addClass(this._canvas, 'leaflet-zoom-hide');
}
this._updateOpacity();
L.extend(this._canvas, {
onselectstart: L.Util.falseFn,
onmousemove: L.Util.falseFn,
onload: L.bind(this._onCanvasLoad, this)
});
},
_animateZoom: function (e) {
var map = this._map,
canvas = this._canvas,
scale = map.getZoomScale(e.zoom),
nw = map.containerPointToLatLng([0, 0]),
se = map.containerPointToLatLng([canvas.width, canvas.height]),
topLeft = map._latLngToNewLayerPoint(nw, e.zoom, e.center),
size = map._latLngToNewLayerPoint(se, e.zoom, e.center)._subtract(topLeft),
origin = topLeft._add(size._multiplyBy((1 / 2) * (1 - 1 / scale)));
L.DomUtil.setTransform(canvas, origin, scale);
},
_reset: function () {
var canvas = this._canvas,
size = this._map.getSize(),
lt = this._map.containerPointToLayerPoint([0, 0]);
L.DomUtil.setPosition(canvas, lt);
canvas.width = size.x;
canvas.height = size.y;
canvas.style.width = size.x + 'px';
canvas.style.height = size.y + 'px';
this.__calcInterval();
this.__draw(true);
},
_onCanvasLoad: function () {
this.fire('load');
},
_updateOpacity: function () {
L.DomUtil.setOpacity(this._canvas, this.options.opacity);
},
__format_coord: function (value, labelMeters, interval) {
// console.debug('__format_coord', {
// value,
// labelMeters,
// interval
// })
var padLength = 3;
switch (interval) {
case 10000:
padLength = 1;
labelMeters = labelMeters / 10000;
break;
case 1000:
padLength = 2;
labelMeters = labelMeters / 1000;
break;
case 100:
padLength = 3;
labelMeters = labelMeters / 100;
break;
}
return `${labelMeters}`.padStart(padLength, '0')
},
__calcInterval: function () {
var zoom = this._map.getZoom();
if (this._currZoom != zoom) {
this._currLngInterval = 0;
this._currLatInterval = 0;
this._currZoom = zoom;
}
var interv;
if (!this._currLngInterval) {
try {
for (var idx in this.options.lngInterval) {
var dict = this.options.lngInterval[idx];
if (dict.start <= zoom) {
if (dict.end && dict.end >= zoom) {
this._currLngInterval = dict.interval;
break;
}
}
}
}
catch (e) {
this._currLngInterval = 0;
}
}
if (!this._currLatInterval) {
try {
for (var idx in this.options.latInterval) {
var dict = this.options.latInterval[idx];
if (dict.start <= zoom) {
if (dict.end && dict.end >= zoom) {
this._currLatInterval = dict.interval;
break;
}
}
}
}
catch (e) {
this._currLatInterval = 0;
}
}
},
__draw: function (label) {
function _parse_px_to_int (txt) {
if (txt.length > 2) {
if (txt.charAt(txt.length - 2) == 'p') {
txt = txt.substr(0, txt.length - 2);
}
}
try {
return parseInt(txt, 10);
}
catch (e) { }
return 0;
};
var self = this,
canvas = this._canvas,
map = this._map,
curvedLon = this.options.lngLineCurved,
curvedLat = this.options.latLineCurved;
if (L.Browser.canvas && map) {
if (!this._currLngInterval || !this._currLatInterval) {
this.__calcInterval();
}
var latInterval = this._currLatInterval,
lngInterval = this._currLngInterval;
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.lineWidth = this.options.weight;
ctx.strokeStyle = this.options.color;
ctx.fillStyle = this.options.fontColor;
ctx.setLineDash(this.options.dashArray);
if (this.options.font) {
ctx.font = this.options.font;
}
var txtWidth = ctx.measureText('0').width;
var txtHeight = 12;
try {
var _font_size = ctx.font.trim().split(' ')[0];
txtHeight = _parse_px_to_int(_font_size);
}
catch (e) { }
var ww = canvas.width,
hh = canvas.height;
var lt = map.containerPointToLatLng(L.point(0, 0));
var rt = map.containerPointToLatLng(L.point(ww, 0));
var rb = map.containerPointToLatLng(L.point(ww, hh));
var _lat_b = rb.lat,
_lat_t = lt.lat;
var _lon_l = lt.lng,
_lon_r = rt.lng;
var _point_per_lat = (_lat_t - _lat_b) / (hh * 0.2);
if (isNaN(_point_per_lat)) {
return;
}
if (_point_per_lat < 1) { _point_per_lat = 1; }
if (_lat_b < -90) {
_lat_b = -90;
}
else {
_lat_b = parseInt(_lat_b - _point_per_lat, 10);
}
if (_lat_t > 90) {
_lat_t = 90;
}
else {
_lat_t = parseInt(_lat_t + _point_per_lat, 10);
}
var _point_per_lon = (_lon_r - _lon_l) / (ww * 0.2);
if (_point_per_lon < 1) { _point_per_lon = 1; }
if (_lon_l > 0 && _lon_r < 0) {
_lon_r += 360;
}
_lon_r = parseInt(_lon_r + _point_per_lon, 10);
_lon_l = parseInt(_lon_l - _point_per_lon, 10);
var ll, latstr, lngstr, _lon_delta = 0.5;
function __draw_lat_line (self, lat_tick, label, interval) {
ll = self._latLngToCanvasPoint(L.latLng(lat_tick, _lon_l));
latstr = self.__format_coord(lat_tick, label, interval);
txtWidth = ctx.measureText(latstr).width;
var spacer = self.options.showLabel && label ? txtWidth + 10 : 0;
if (curvedLat) {
if (typeof (curvedLat) == 'number') {
_lon_delta = curvedLat;
}
var __lon_left = _lon_l, __lon_right = _lon_r;
if (ll.x > 0) {
var __lon_left = map.containerPointToLatLng(L.point(0, ll.y));
__lon_left = __lon_left.lng - _point_per_lon;
ll.x = 0;
}
var rr = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_right));
if (rr.x < ww) {
__lon_right = map.containerPointToLatLng(L.point(ww, rr.y));
__lon_right = __lon_right.lng + _point_per_lon;
if (__lon_left > 0 && __lon_right < 0) {
__lon_right += 360;
}
}
ctx.beginPath();
ctx.moveTo(ll.x + spacer, ll.y);
var _prev_p = null;
for (var j = __lon_left; j <= __lon_right; j += _lon_delta) {
rr = self._latLngToCanvasPoint(L.latLng(lat_tick, j));
ctx.lineTo(rr.x - spacer, rr.y);
if (self.options.showLabel && label && _prev_p != null) {
if (_prev_p.x < 0 && rr.x >= 0) {
var _s = (rr.x - 0) / (rr.x - _prev_p.x);
var _y = rr.y - ((rr.y - _prev_p.y) * _s);
ctx.fillText(latstr, 0, _y + (txtHeight / 2));
}
else if (_prev_p.x <= (ww - txtWidth) && rr.x > (ww - txtWidth)) {
var _s = (rr.x - ww) / (rr.x - _prev_p.x);
var _y = rr.y - ((rr.y - _prev_p.y) * _s);
ctx.fillText(latstr, ww - txtWidth, _y + (txtHeight / 2) - 2);
}
}
_prev_p = { x: rr.x, y: rr.y, lon: j, lat: i };
}
ctx.stroke();
}
else {
var __lon_right = _lon_r;
var rr = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_right));
if (curvedLon) {
__lon_right = map.containerPointToLatLng(L.point(0, rr.y));
__lon_right = __lon_right.lng;
rr = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_right));
var __lon_left = map.containerPointToLatLng(L.point(ww, rr.y));
__lon_left = __lon_left.lng;
ll = self._latLngToCanvasPoint(L.latLng(lat_tick, __lon_left));
}
ctx.beginPath();
ctx.moveTo(1 + spacer, ll.y);
ctx.lineTo(rr.x - 1 - spacer, rr.y);
ctx.stroke();
if (self.options.showLabel && label) {
var _yy = ll.y + (txtHeight / 2) - 2;
ctx.fillText(latstr, 0, _yy);
ctx.fillText(latstr, ww - txtWidth, _yy);
}
}
};
if (latInterval > 0) {
// this is hacky but serves a purpose of allowing interval options to be specified in meters (EPSG:3857).
// we want this to specify 10km, 1km, and 100m grid squares, so we need to convert the interval to degrees so that this tool will work, as it only takes degrees.
var latIntervalMeters = +latInterval;
var latIntervalLabel = 0;
var tgtPoint = proj4('EPSG:3857', 'EPSG:4326', [0, latInterval])
var latIntervalDegrees = turf.distance([0, 0], tgtPoint, { units: 'degrees' })
// console.debug('Lat grid calculations', {
// latIntervalMeters,
// latIntervalDegrees,
// tgtPoint,
// })
// draw 0 lat line
__draw_lat_line(this, 0, 0);
// draw positive lat lines
for (var i = 0; i <= _lat_t; i += latIntervalDegrees, latIntervalLabel += latIntervalMeters) {
if (i >= _lat_b) {
__draw_lat_line(this, i, latIntervalLabel, latIntervalMeters);
}
}
// draw negative lat lines
latIntervalLabel = 0;
for (var i = 0; i >= _lat_b; i -= latIntervalDegrees, latIntervalLabel -= latIntervalMeters) {
if (i <= _lat_t) {
__draw_lat_line(this, i, latIntervalLabel, latIntervalMeters);
}
}
}
function __draw_lon_line (self, lon_tick, label, interval) {
lngstr = self.__format_coord(lon_tick, label, interval);
txtWidth = ctx.measureText(lngstr).width;
var bb = self._latLngToCanvasPoint(L.latLng(_lat_b, lon_tick));
var spacer = self.options.showLabel && label ? txtHeight + 5 : 0;
if (curvedLon) {
if (typeof (curvedLon) == 'number') {
_lat_delta = curvedLon;
}
ctx.beginPath();
ctx.moveTo(bb.x, 5 + spacer);
var _prev_p = null;
for (var j = _lat_b; j < _lat_t; j += _lat_delta) {
var tt = self._latLngToCanvasPoint(L.latLng(j, lon_tick));
ctx.lineTo(tt.x, tt.y - spacer);
if (self.options.showLabel && label && _prev_p != null) {
if (_prev_p.y > 8 && tt.y <= 8) {
ctx.fillText(lngstr, tt.x - (txtWidth / 2), txtHeight + 5);
}
else if (_prev_p.y >= hh && tt.y < hh) {
ctx.fillText(lngstr, tt.x - (txtWidth / 2), hh - 2);
}
}
_prev_p = { x: tt.x, y: tt.y, lon: lon_tick, lat: j };
}
ctx.stroke();
}
else {
var __lat_top = _lat_t;
var tt = self._latLngToCanvasPoint(L.latLng(__lat_top, lon_tick));
if (curvedLat) {
__lat_top = map.containerPointToLatLng(L.point(tt.x, 0));
__lat_top = __lat_top.lat;
if (__lat_top > 90) { __lat_top = 90; }
tt = self._latLngToCanvasPoint(L.latLng(__lat_top, lon_tick));
var __lat_bottom = map.containerPointToLatLng(L.point(bb.x, hh));
__lat_bottom = __lat_bottom.lat;
if (__lat_bottom < -90) { __lat_bottom = -90; }
bb = self._latLngToCanvasPoint(L.latLng(__lat_bottom, lon_tick));
}
ctx.beginPath();
ctx.moveTo(tt.x, 5 + spacer);
ctx.lineTo(bb.x, hh - 1 - spacer);
ctx.stroke();
if (self.options.showLabel && label) {
ctx.fillText(lngstr, tt.x - (txtWidth / 2), txtHeight + 5);
ctx.fillText(lngstr, bb.x - (txtWidth / 2), hh - 3);
}
}
};
if (lngInterval > 0) {
// this is hacky but serves a purpose of allowing interval options to be specified in meters (EPSG:3857).
// we want this to specify 10km, 1km, and 100m grid squares, so we need to convert the interval to degrees so that this tool will work, as it only takes degrees.
var lngIntervalMeters = +lngInterval;
var lngIntervalLabel = 0;
var tgtPoint = proj4('EPSG:3857', 'EPSG:4326', [lngInterval, 0])
var lngIntervalDegrees = turf.distance([0, 0], tgtPoint, { units: 'degrees' })
// console.debug('lng grid calculations', {
// lngIntervalMeters,
// lngIntervalDegrees,
// tgtPoint,
// })
// draw positive lng lines
for (var i = 0; i <= _lon_r; i += lngIntervalDegrees, lngIntervalLabel += lngIntervalMeters) {
if (i >= _lon_l) {
__draw_lon_line(this, i, lngIntervalLabel, latIntervalMeters);
}
}
}
}
},
_latLngToCanvasPoint: function (latlng) {
var map = this._map;
var projectedPoint = map.latLngToLayerPoint(L.latLng(latlng));
// console.debug('_latLngToCanvasPoint latlng', latlng);
// console.debug('_latLngToCanvasPoint projectedPoint', projectedPoint);
var finalPoint = L.point(projectedPoint).add(map._getMapPanePos());
// console.debug('_latLngToCanvasPoint finalPoint', finalPoint);
return finalPoint;
}
});
L.latlngGraticule = function (options) {
return new L.LatLngGraticule(options);
};
}(this, document));
/**
* Display mouse MGRS coordinates on map
*
* Author: jetelain
*/
L.Control.GridMousePosition = L.Control.extend({
options: {
position: 'topright',
precision: 4
},
onAdd: function (map) {
this._container = L.DomUtil.create('div', 'leaflet-grid-mouseposition');
L.DomEvent.disableClickPropagation(this._container);
map.on('mousemove', this._onMouseMove, this);
var placeHolder = '0'.repeat(this.options.precision);
this._container.innerHTML = `Grid: ${placeHolder} - ${placeHolder}`;
return this._container;
},
onRemove: function (map) {
map.off('mousemove', this._onMouseMove)
},
_onMouseMove: function (e) {
var coord_4326 = e.latlng;
var mgrsStr = latlngToMGRS(coord_4326.lat, coord_4326.lng, this.options.precision);
var mousePositionXY = mgrsStr[0] + " " + mgrsStr[1];
// console.debug(mgrsStr);
// console.debug(mousePositionXY)
this._container.innerHTML = `Grid: ${mousePositionXY}`;
}
});
L.control.gridMousePosition = function (options) {
return new L.Control.GridMousePosition(options);
};
function latLngTo3857 (lat, lng) {
return proj4("EPSG:4326", "EPSG:3857", [lng, lat]);
}
function latlngToMGRS (lat, lng, precision = 4) {
// console.debug("latlngToMGRS", lat, lng, precision);
var coord_3857 = latLngTo3857(lat, lng);
// console.debug("coord_3857", coord_3857)
var mercatorStr = [
Math.abs(coord_3857[0] / 10).toFixed(0).toString(),
Math.abs(coord_3857[1] / 10).toFixed(0).toString()
];
mercatorStr = mercatorStr.map((coord) => {
return `${coord}`.padStart(precision, '0')
});
// * add negative sign if needed
coord_3857[0] < 0
? (mercatorStr[0] = "-" + mercatorStr[0])
: (mercatorStr[0] = mercatorStr[0]);
coord_3857[1] < 0
? (mercatorStr[1] = "-" + mercatorStr[1])
: (mercatorStr[1] = mercatorStr[1]);
return mercatorStr;
}

27
src/js/sessionMgmt.js Normal file
View File

@@ -0,0 +1,27 @@
// joinSession is initiated from form submission on the main page
const joinSession = (e) => {
e.preventDefault();
// Get all the form data
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
// Send the form data to RabbitMQ
fetch('/joinSession', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then((res) => res.json())
.then((res) => {
// If the response is successful, redirect to the session page
if (res.success) {
window.location.href = `/session/${res.sessionId}`;
} else {
// If the response is not successful, display an error message
alert(res.message);
}
});
}

53
src/login.html Normal file
View File

@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<link href="index.css" rel="stylesheet">
<script defer src="index.js"></script>
</head>
<body class="bg-gray-100">
<h1 class="text-3xl text-center font-bold mt-8">Login</h1>
<form id="loginForm" class="max-w-md mx-auto mt-8 p-4 bg-white rounded-lg shadow-md">
<div class="form-group mb-4">
<label for="username" class="block font-bold">Username</label>
<input type="text" id="username" name="username" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div class="form-group mb-4">
<label for="password" class="block font-bold">Password</label>
<input type="password" id="password" name="password" required class="w-full px-3 py-2 border border-gray-300 rounded-md">
</div>
<div class="form-group">
<button type="submit" class="px-4 py-2 bg-blue-500 text-white font-bold rounded-md">Login</button>
</div>
<div id="message" class="form-group"></div>
</form>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
$(document).ready(function () {
$('#loginForm').submit(function (e) {
e.preventDefault();
var username = $('#username').val();
var password = $('#password').val();
$.ajax({
url: '/login',
type: 'POST',
dataType: 'json',
data: JSON.stringify({ username: username, password: password }),
contentType: 'application/json',
success: function (data) {
$('#message').html('<div class="success-message text-green-500">Login successful.</div>');
// Redirect to the desired page after successful login
window.location.href = '/';
},
error: function (xhr, status, error) {
$('#message').html('<div class="error-message text-red-500">Invalid credentials.</div>');
}
});
});
});
</script>
</body>
</html>

21
src/logout.html Normal file
View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>Login</title>
<link href="index.css" rel="stylesheet">
<script defer src="index.js"></script>
<meta http-equiv="refresh" content="1;url=/">
</head>
<body class="bg-gray-100">
<h1 class="text-3xl text-center font-bold mt-8">Ausgeloggt</h1>
<p class="text-1xl text-center font-bold mt-8">Du hast dich erfolgreich ausgelogt.</p>
<script>
$(document).ready(function () {
$.ajax({
url: '/logout',
type: 'POST',
});
});
</script>
</body>
</html>