change to GORM, add member and rank handlers
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
|
||||
10
api/go.sum
@@ -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=
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
})
|
||||
}
|
||||
BIN
src/css/images/layers-2x.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/css/images/layers.png
Normal file
|
After Width: | Height: | Size: 696 B |
BIN
src/css/images/marker-icon-2x.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
src/css/images/marker-icon.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/css/images/marker-shadow.png
Normal file
|
After Width: | Height: | Size: 618 B |
BIN
src/css/images/spritesheet-2x.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
src/css/images/spritesheet.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
156
src/css/images/spritesheet.svg
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
628
src/js/mapUtils.js
Normal 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
@@ -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
@@ -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
@@ -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>
|
||||