From 3b715cf331942412c320284bd17274731846b45a Mon Sep 17 00:00:00 2001 From: IndigoFox Date: Fri, 8 Mar 2024 00:39:46 -0800 Subject: [PATCH] change to GORM, add member and rank handlers --- .env.example | 6 +- api/db/award.go | 110 +++++ api/db/course.go | 142 ++++++ api/db/main.go | 49 +- api/db/member.go | 219 +++++++++ api/db/rank.go | 109 +++++ api/db/trainingEvent.go | 113 +++++ api/go.mod | 4 + api/go.sum | 10 + api/main.go | 7 + api/ops/member.go | 198 -------- api/routes/handler.go | 23 +- api/routes/member.go | 141 ++++++ api/routes/rank.go | 127 +++++ go.work | 2 +- src/css/images/layers-2x.png | Bin 0 -> 1259 bytes src/css/images/layers.png | Bin 0 -> 696 bytes src/css/images/marker-icon-2x.png | Bin 0 -> 2586 bytes src/css/images/marker-icon.png | Bin 0 -> 1466 bytes src/css/images/marker-shadow.png | Bin 0 -> 618 bytes src/css/images/spritesheet-2x.png | Bin 0 -> 3581 bytes src/css/images/spritesheet.png | Bin 0 -> 1906 bytes src/css/images/spritesheet.svg | 156 ++++++ src/css/input.css | 6 + src/css/leaflet.draw.css | 10 + src/css/mapUtils.css | 117 +++++ src/draw.html | 34 ++ src/index.css | 778 ++++++++++++++++++++++++++++++ src/index.html | 58 +++ src/index.js | 11 + src/js/defaultMap.js | 552 +++++++++++++++++++++ src/js/leaflet.draw.js | 10 + src/js/mapUtils.js | 628 ++++++++++++++++++++++++ src/js/sessionMgmt.js | 27 ++ src/login.html | 53 ++ src/logout.html | 21 + 36 files changed, 3491 insertions(+), 230 deletions(-) create mode 100644 api/db/award.go create mode 100644 api/db/course.go create mode 100644 api/db/member.go create mode 100644 api/db/rank.go create mode 100644 api/db/trainingEvent.go delete mode 100644 api/ops/member.go create mode 100644 api/routes/member.go create mode 100644 api/routes/rank.go create mode 100644 src/css/images/layers-2x.png create mode 100644 src/css/images/layers.png create mode 100644 src/css/images/marker-icon-2x.png create mode 100644 src/css/images/marker-icon.png create mode 100644 src/css/images/marker-shadow.png create mode 100644 src/css/images/spritesheet-2x.png create mode 100644 src/css/images/spritesheet.png create mode 100644 src/css/images/spritesheet.svg create mode 100644 src/css/input.css create mode 100644 src/css/leaflet.draw.css create mode 100644 src/css/mapUtils.css create mode 100644 src/draw.html create mode 100644 src/index.css create mode 100644 src/index.html create mode 100644 src/index.js create mode 100644 src/js/defaultMap.js create mode 100644 src/js/leaflet.draw.js create mode 100644 src/js/mapUtils.js create mode 100644 src/js/sessionMgmt.js create mode 100644 src/login.html create mode 100644 src/logout.html diff --git a/.env.example b/.env.example index 5dd5f1e..7de5f28 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ -mariadb.connectionstring=user:password@tcp(localhost:3306)/dbname?parseTime=true -api.prefix=/api/v1 -api.port=1323 \ No newline at end of file +MARIADB_CONNSTRING=user:password@tcp(localhost:3306)/dbname?parseTime=true +API_PREFIX=/api/v1 +API_PORT=1323 \ No newline at end of file diff --git a/api/db/award.go b/api/db/award.go new file mode 100644 index 0000000..1dfaf8a --- /dev/null +++ b/api/db/award.go @@ -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 +} diff --git a/api/db/course.go b/api/db/course.go new file mode 100644 index 0000000..5ec8b9d --- /dev/null +++ b/api/db/course.go @@ -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 +} diff --git a/api/db/main.go b/api/db/main.go index 57628f6..bdec255 100644 --- a/api/db/main.go +++ b/api/db/main.go @@ -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 } diff --git a/api/db/member.go b/api/db/member.go new file mode 100644 index 0000000..b65310e --- /dev/null +++ b/api/db/member.go @@ -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 +} diff --git a/api/db/rank.go b/api/db/rank.go new file mode 100644 index 0000000..363f2c5 --- /dev/null +++ b/api/db/rank.go @@ -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 +} diff --git a/api/db/trainingEvent.go b/api/db/trainingEvent.go new file mode 100644 index 0000000..828f11a --- /dev/null +++ b/api/db/trainingEvent.go @@ -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 +} diff --git a/api/go.mod b/api/go.mod index 7d95114..5ea5155 100644 --- a/api/go.mod +++ b/api/go.mod @@ -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 ) diff --git a/api/go.sum b/api/go.sum index 267e71f..b146366 100644 --- a/api/go.sum +++ b/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= diff --git a/api/main.go b/api/main.go index d8dd197..9005435 100644 --- a/api/main.go +++ b/api/main.go @@ -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, diff --git a/api/ops/member.go b/api/ops/member.go deleted file mode 100644 index 05257da..0000000 --- a/api/ops/member.go +++ /dev/null @@ -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) -} diff --git a/api/routes/handler.go b/api/routes/handler.go index ad65265..c5c8452 100644 --- a/api/routes/handler.go +++ b/api/routes/handler.go @@ -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) } diff --git a/api/routes/member.go b/api/routes/member.go new file mode 100644 index 0000000..ffb5ea8 --- /dev/null +++ b/api/routes/member.go @@ -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) + }) +} diff --git a/api/routes/rank.go b/api/routes/rank.go new file mode 100644 index 0000000..62b4d49 --- /dev/null +++ b/api/routes/rank.go @@ -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) + }) +} diff --git a/go.work b/go.work index 3f8ca95..2d30281 100644 --- a/go.work +++ b/go.work @@ -1,3 +1,3 @@ -go 1.22.0 +go 1.22 use ./api \ No newline at end of file diff --git a/src/css/images/layers-2x.png b/src/css/images/layers-2x.png new file mode 100644 index 0000000000000000000000000000000000000000..200c333dca9652ac4cba004d609e5af4eee168c1 GIT binary patch literal 1259 zcmVFhCYNy;#0irRPomHqW|G1C*;4?@4#E?jH>?v@U%cy?3dQAc-DchXVErpOh~ z-jbon+tNbnl6hoEb;)TVk+%hTDDi_G%i3*RZ&15!$Fjr^f;Ke&A@|?=`2&+{zr+3a z{D*=t(`AXyS%X7N z%a#RZw6vD^t_rnM`L4E>m=U&R!A-&}nZIi$BOPvkhrCuUe@BN~-lRD)f44;J%TwgE zcze8u!PQ_NR7?o(NylLXVTfDO zxs5=@|GsYEsNo4M#nT%N!UE(?dnS)t2+{ELYAFp*3=iF=|EQnTp`#vlSXuGVraYo? z+RCzXo6h3qA8{KG?S4nE(lM+;Eb4nT3XV;7gcAxUi5m)`k5tv}cPy()8ZR3TLW3I- zAS^}cq-IJvL7a4RgR!yk@~RT%$lA7{L5ES*hyx)M4(yxI$Ub(4f)K|^v1>zvwQY!_ zIrWw8q9GS^!Dp~}+?mbnB6jDF8mVlbQ!jFKDY;w=7;XO{9bq7>LXGK24WA`;rL)_Z z)&j}pbV(;6gY;VMhbxgvn`X;6x}VUEE-7 z%)7j-%t8S=ZL3yc)HbXDAqJZvBTPoiW_A-+a8m3_Z?v{DN7Tnr#O_VUMT0UBt$;p` zDh6JbGHN8JJ*JN%y2%msb97@_S>9!%Egwk;?PEkU9ntz&3uR}%Fj5d$JHQbQb3}a{ zSzFT^#n=VInPpcAS}CNxj?_ zVscANk5Cfz(51EI1pz};AWWb|kgbYNb4wCEGUn3+eMUMV?1-{=I4TlmLJMot@rd07 zZuo2hk1ccu{YmGkcYdWAVdk{Z4Nm?^cTD&}jGm+Q1SYIXMwmG*oO*83&#>l%nbR`G zhh=lZ%xIb7kU3#;TBbfECrnC9P=-XpL|TG2BoZdj61*XiFbW8?1Z_wp%#;>${SUIy V$8qr;L*)Pf002ovPDHLkV1hYLS~36t literal 0 HcmV?d00001 diff --git a/src/css/images/layers.png b/src/css/images/layers.png new file mode 100644 index 0000000000000000000000000000000000000000..1a72e5784b2b456eac5d7670738db80697af3377 GIT binary patch literal 696 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 UQO-*oc+)|Q_08}VdXD6O0C*xx%>V!Z literal 0 HcmV?d00001 diff --git a/src/css/images/marker-icon.png b/src/css/images/marker-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..950edf24677ded147df13b26f91baa2b0fa70513 GIT binary patch literal 1466 zcmV;r1x5OaP)P001cn1^@s6z>|W`000GnNklGNuHDcIX17Zdjl&3`L?0sTjIws<{((Dh&g-s0<@jYQyl?D*X^?%13;ml^gy> ziMrY_^1WI=(g@LMizu=zCoA>C`6|QEq1eV92k*7m>G65*&@&6)aC&e}G zI)pf-Za|N`DT&Cn1J|o`19mumxW~hiKiKyc-P`S@q)rdTo84@QI@;0yXrG%9uhI>A zG5QHb6s4=<6xy{1 z@NMxEkryp{LS44%z$3lP^cX!9+2-;CTt3wM4(k*#C{aiIiLuB>jJj;KPhPzIC00bL zU3a#;aJld94lCW=`4&aAy8M7PY=HQ>O%$YEP4c4UY#CRxfgbE~(|uiI=YS8q;O9y6 zmIkXzR`}p7ti|PrM3a}WMnR=3NVnWdAAR>b9X@)DKL6=YsvmH%?I24wdq?Gh54_;# z$?_LvgjEdspdQlft#4CQ z`2Zyvy?*)N1Ftw|{_hakhG9WjS?Az@I@+IZ8JbWewR!XUK4&6346+d#~gsE0SY(LX8&JfY>Aj)RxGy96nwhs2rv zzW6pTnMpFkDSkT*a*6Dx|u@ds6ISVn0@^RmIsKZ5Y;bazbc;tTSq(kg(=481ODrPyNB6n z-$+U}(w$m6U6H$w17Bw+wDaFIe~GvNMYvnw31MpY0eQKT9l>SU``8k7w4)z!GZKMI z#_cEKq7k~i%nlK@6c-K?+R;B#5$?T#YpKD`t_4bAs^#E+@5QW$@OX3*`;(#{U^d-vY)&xEE>n5lYl&T?Amke9$Lam@{1K@O ze*LXqlKQHiv=gx+V^Cbb2?z@ISBQ*3amF;9UJ3SBg(N|710TLamQmYZ&Qjn2LuO<* zCZlB4n%@pc&7NNnY1}x+NWpHlq`OJEo|`aYN9<`RBUB+79g;>dgb6YlfN#kGL?lO_ z!6~M^7sOnbsUkKk<@Ysie&`G>ruxH&Mgy&8;i=A zB9OO!xR{AyODw>DS-q5YM{0ExFEAzt zm>RdS+ssW(-8|?xr0(?$vBVB*%(xDLtq3Hf0I5yFm<_g=W2`QWAax{1rWVH=I!VrP zs(rTFX@W#t$hXNvbgX`gK&^w_YD;CQ!B@e0QbLIWaKAXQe2-kkloo;{iF#6}z!4=W zi$giRj1{ zt;2w`VSCF#WE&*ev7jpsC=6175@(~nTE2;7M-L((0bH@yG}-TB$R~WXd?tA$s3|%y zA`9$sA(>F%J3ioz<-LJl*^o1|w84l>HBR`>3l9c8$5Xr@xCiIQ7{x$fMCzOk_-M=% z+{a_Q#;42`#KfUte@$NT77uaTz?b-fBe)1s5XE$yA79fm?KqM^VgLXD07*qoM6N<$ Ef<_J(9smFU literal 0 HcmV?d00001 diff --git a/src/css/images/spritesheet-2x.png b/src/css/images/spritesheet-2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c45231aff86b1344333414cdfa7a2a74d38d5304 GIT binary patch literal 3581 zcmb_f`#;nBAOB2clxyUYb<>4Ynk1K8OH(Z&mzm3$Tyx3X$HJB_R+xiO7)ip%jA&_M zl!g=0DI?9L49i_)E<@k-{pI@)d|!{p`}%mjuaEcZ`Fy_bU9q_!y?ys~005+s7ZLUV zAm%Kpn@EU@^7{ODOi{5l!UE|iA+k6LAF`+|8G6wrLge4~CqXPU^I}Aus*#q?kq$V& zNc`1sUjUEC!vjKsBD}AL`oeMH{&}+|yG7mvB;u@NO#aLuF$Lxy3!R_SjbJ6a7tRHv zBz1~*mIVgf1VOjVip+7Z;f4r4FSwLrXXkj}z+=1B1NW~XOT>;Iw+;<}CA@r#lE0(? zd3i0IzewW74Wp>ECohsvqkk$E}Ms%B2GGcE&2IsxA>@@3X3m zDG^w?t(hOXlBT_cpS&2Dn|fFVwPcdu>G`b>)^_?a2;I}xzh~>4em_0}F`+6N=b))5 zLvEh|>p9CCdt9b&^Nrhjjm187ePKOPzjf=DLC&QTs(QE@DG$KNeuI&6ASNZCEEYc) z^=4&drTg>cr(@e`G~Lz)@iTjygDgj_!8H%>P9nM8^sHQdKsdqY=A2N?F-n=V2k=N+ zfT{pCVlg&|RwE;$y}1t`8uq<7bbQgI+mNCQf^{R4P0N-*8sv-e-K#wf)44&Bkv)x} z-`}1E>QMTXP8^-?#Rkp$tp3h{vasOc>3I`eGI`o2V^r3XUao;FTaV2oHxysdT<=-l zAV$OZEDRid@nB)L+Ah%PGbix1ApRKl<#nz!eQMlpHZo`z5XX)hB({%*YvdT{-Uudg z|FL-Ow6wHTIXgSMDMxC8REOLg)GnKxotT&~b#QRV$vM#IC?zAa>6dZn24tYVnVyz? zsRVDn(0*_jOKAvJ-MxD<_t7J!LNnKLR2t~j3T;nTuhEl8 zq+Wdb5#@StSLM2@pHQEk#jOkc{LH#SKlsbMi#)ToX5s;ufI`UB(}f$1pgvZdV7h-v zz2@Jrdp2$H9E>$~WyDvk zt*tp#xz&YWP<4(?*7(wbLLiYg>jn}mgiijyQ;=2i2Uj81L3JtlR_7o)yz zH)gs}C?qm!YZoO*2Jn&UCtw06YS4DCQRI&s0b_pLOIp=OH71JzkH&X$8vcHMA!cza z^MB>VmXx0ik5cSYLSKMfMtLklUNOh{e8;5^c8VLguc!6Bu3amua6D>ybIo{I0huvi zcKY<{xo?yCFrYi>>WXq%S?YUIgMLUlY^>Sz3K2r6EUfqcG`R2H*Ju0k6T5X8Wf(};_CV?wb+smDvO#Pu`cr_B=a3D zCk$?wcQ3w7ZodT(K(3?SKFfb+Ix;6dsd$?K-*h?4AfbsoAnVy>C)pQe>uB3hYcxFB z6gD?)^DsAU=Jm-H7O@oEJVr{UPYK4tnf}UZ#r7%tARkLX1v|w?wo`)j+`zahlb}W6 z&8)^H)VsuTygfZ1UqC!LjM2}vdIAcIHP;+=Jh!j?v|X!v(d?*tVYT8cuH{5Sn?0>Z z#oGk9`eA&A!A51x30pvdr$8UIhy3J6`it=32Rh?!fIfOC0WQ@d+Y>ub1sFl3s;P`l)3e7w zn%~@l5@{fQ3yB=pb3WmW!VVfu8J%@t^sB5_@kxV;Mr!%w9_N*iH|!txcrm5ynDR0$ z_6gxeV%?MEcAj2dFi}6)%`IO3?)NmMud$Sl^C;VUZLmwzk+Ssc%Z&olE*&`X$Zp!^8B9jJ~>PDhha_Ra+2GOEko*&K!KX z<$k*iSI40`>^Zb7aOPfYxxv;`E!vGw?Q#66*r98#lJ@7mu&%c*Z%AtndqyxTDk`Fl zS~461(~tn2EcKPloZjY?oqP;6ob$d9a~`K2w=xK4vaS> z>mxIMzF5SF_YGK&8+LtzC10-Z$dYqm3qH_wf&J%C8fkHkz>O~p%vP+jHDk-2wOk*?e6=`v!$6gD zXD08sxy5Q=GY-`Yuk}>`$mFcmkL60vGtL;unK8Al(&I^EV{dxB;}y>%T7%nOR4(|e zb{oizu%cIL9)pGU99zhW&y4TE_hv6~(WtM1+nWPXdiq{Y+Zoq^OB5cgMqZa;b$iAY zgL1EroM4GatZ8d#W@-%hUaW#mZa)VSwEaxQpPl8SD_uDY+mwJ;bm1NQ@?QxU>Sfm| zUHVjlPHKL(o1!IN&lq}1mTSOuTvAi7VTa|*LRPBI^X|0^9itfF_;u5?6It@KXl**+ z85lTO18x>q#!z_G&4D^1%13bqUyhVd4k{=p@PpY_4)C1Un(E23B*f@JCT7-a_i1@~ z)SqC0#2`_)pYcsa8W%($Rtye_f}Qt;a4NS^tV5-kV}8ftyMK&tSk%*silA!u`xW5O z&=3(ipI;KHRqgsw#4mdtd(Q`aRA77ktW_G(%G)n7dtsFsykw-Z^^G20@nsn{Klx~~ z5*+#AdCuPondXp zTH4~hI|Vabv^^)yZF`$o#WrheYxih(yfVo_`u6E^5bqP}k#WDhaEqZ`8>@80FafGH zIMg6TL1W7%Gtkqv89svd>uvnO^lUXQew###hytMi&4CjtGP`bZK|JwXxuX$j0i4%< z7q)Jbx9Xx@9a!k!W0SLuzti&H1YkJuM2bg4%+DnKg#*HH<^U2kNCR!*0%!SHBV@*L zJ4MF&f>eZL;x`)4@E{O~b1$6PNusXCZEAEp$d;DvBs8|E)c&yfqN1y=-V`H~3;Ox~ z3OVd4q)Y8?`}z7x!n+Txhmf0Gl?XHzJ3M(u=2tw?e=OgOv``~u99-C9kA+rHlPk9G zN^f%DTuX0bYhpd(&Z7$Rb93wSEkb;dik79qwY88bkxtrAO7r}FZ;o#{T3}H$|4530 zX?$CBE{VkxZkB-*NSSGvyHdKjOk#7ZZ;vmpepVzWKN&;T;iM&?HAROH^aw5aQCU3T z!H$W7f`UeYZb^w~pD@u`E)j{pub|O=@gha}NA2UAhE*1$A_luWw0n(Bp602PmQ(vEjZrBI>^%!!+W>I=~TZtF*Peo+n(heqw2o|zfU>yG^`j4kB5k|9F|j&*W=71 z=W_?-9ZS7Vt%W>>7E?d0Mf0xf{queA>-yf`?|on2KR(yzzCYI|^`awYx4gPM0ARPh z9U2D!?2!14kd+qaoK_uw@wFr3ti6k@IAUe5rHE^}P`k?!07!rSd0>c0^AqCce)2gy z**Szrj=mc12cn~+jRQ!55x!SL{ftAx{qt8&tBZg0?a|gQl!9fpN06qQip=Wv)D%Y- z+bg+w(aFW?lgyt1|9)ik>q%L+jrR?%LpEbnIpOt#PI47gtD8Xgkp?04z@vSC?o-yT zs)RLFQ&Mk)a7XZwPQ1fXhWF4FZ(?J55vMD5sr6?s^WZS?6c1xEs1ktlD89lbT4wr7L0U z?Ey%L8UfB3 z6Oo*h^2-)Lt*E^xr-tS4g2z6x4@=;H7tUP;O04r6_s<$_v*!eF15ymD~r9(3MS4qmRge#Kgp?GsW8{XrmeD>HT?B zYsDmcE>bm3*w)sDVF&?Dp~-{b&E1rimUhg+>dm#UI1$hEGX>wex1SLpMe9RhFR<5~;%m8wdW36OCQws-U? zbNLM@^k(Q8_sGqZ=lG#KD_po)L;L4B_Yzc8kXpSterHwlZVn25JZCp>)@&EDDq6c_LRD2YN?%BUQzu*j1W4nA z9ywrRV`C;B)G698=(YVU$kX1a9QR2JR1$cbY<8(uUpe?J6ROf%NZ|G8XPXUqoE5lY zN~-R9YVZdK2M5wk>hL*Djkt=fz&7)NFa!t{v(VA8eseemAE1UD2#Zo0-O;Xw1a{=e zCa0eZ)~<{M-duKg+j}+OpqI`5s~Sd>ra?76Qoz~SdD{hBC{sTGhb%y<${}DTVb1>W zy4h!1VdgNg+bn;0#2~Mc)9~QChvS926L9BiTz@yCOTe4GL4hYvyJ0y`n!2Cjg17|I z6Z|1UfGQFkCH9^uvf9jEue9D~IOsLR8WO&)?r#I#Jhs#P~1v1k(GWp2GNdUiy& zFkVOT{t}{T5Kh1T#hW`m6!3@N<=edW%?CKI=0nKdZO^E!Gp`B?xm+%y|Ks0XLrl`s z*)3Pl@rK#qbaIoDn!>Oc&yh2A)~7Fh7xYvnjD1O4b*Q;ydC#^%0o^lh2|yEs&m7VGd*3r@g*4vB#4OMn2LG^OP=)U z%Q2`;tltV!*h`;LiFz^LwGe6pF^+p_G;C@;AKtdqPD*7I-`(47h>#0xdIt$(MG`dk zj-;Zx30`0vhjSYitI?ZPm&*|5m*EUIG;JAAHMJ%<2 z>h;tap8Q9i@Ja!MZ@7fV8t!>s51Tn)nkiUcU-rtBOfOo<-aBWaoBOZeI|(OHD9+J- zO<4V#$CFY3^X8if#me{Q3N&4=al@ts2J{h`nPYpYt>>Bl03yB@A9L$QC!J0Y0hE1K zH_7C+ALn1T)^@1%3P9{($KnPze2vZH=3p&Ho=K>j1A6+-h~D9OBbsT9V5RH01*1Qf z5O1y6xoAFeQJ=owm&-n7Ts-hmyMyuc>jZCI1`=-7fR#HBJ!>a;3je&1G|Iqa;~@OU zFU2V&e{Ld(%d-`&j|a|4(L^E^+csfMgU~=v&b-S#u zudmR(Iq2qsS45tW+~pasy_~(gwzkGDEiENAPH9TgYTR28V$BxX8nc^wOf4;U9y~>R z=_cYoV~U4^X$~Ghm30w3SbBPl>WTiHVD#iOn_Aw7-7~XdSf_VAR{)g66#YCQJ%5%~ i{eP_FzxHA`84t@4c6%tZ%} + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/css/input.css b/src/css/input.css new file mode 100644 index 0000000..d0dbc32 --- /dev/null +++ b/src/css/input.css @@ -0,0 +1,6 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@import 'leaflet.draw.css'; +@import 'mapUtils.css'; \ No newline at end of file diff --git a/src/css/leaflet.draw.css b/src/css/leaflet.draw.css new file mode 100644 index 0000000..a019410 --- /dev/null +++ b/src/css/leaflet.draw.css @@ -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} \ No newline at end of file diff --git a/src/css/mapUtils.css b/src/css/mapUtils.css new file mode 100644 index 0000000..6bd93ed --- /dev/null +++ b/src/css/mapUtils.css @@ -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+'); +} \ No newline at end of file diff --git a/src/draw.html b/src/draw.html new file mode 100644 index 0000000..5531551 --- /dev/null +++ b/src/draw.html @@ -0,0 +1,34 @@ + + + + + + + Arma 3 Planner + + + + + +
+
+
+
+
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..a30870e --- /dev/null +++ b/src/index.css @@ -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)); +} \ No newline at end of file diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..2cf41b3 --- /dev/null +++ b/src/index.html @@ -0,0 +1,58 @@ + + + + + + + + + Arma 3 Planner + + + + + + +
+
+

Arma 3 Planner

+ +
+ + +
+ + +
+ + + + +
+ +
+ or +
+
+ +
+ + + + +
+
+
+
+
+ \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..66467da --- /dev/null +++ b/src/index.js @@ -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"; \ No newline at end of file diff --git a/src/js/defaultMap.js b/src/js/defaultMap.js new file mode 100644 index 0000000..6903fe9 --- /dev/null +++ b/src/js/defaultMap.js @@ -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 = '

Recent Sessions

    '; + for (let session of recentSessions || []) { + if (session === recentSessions[0]) { + // if first, add a "current" label + recentSessionsHtml += '
  • Current: ' + session + '
  • '; + } else { + // otherwise, add normally + recentSessionsHtml += '
  • ' + session + '
  • '; + } + // skip any past 7 + if (session === recentSessions[6]) { + break; + } + } + recentSessionsHtml += '
'; + + 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 = `${marker.name}`; + 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: ` +

Marker URL

+ +

You can also click here to select a marker image

+ + + `, + 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 = $(''); + var logout = $('
'); + + // Create the import button + var importButton = $('
'); + $('.leaflet-pm-toolbar:last').append(importButton); + + // Create the export button + var exportButton = $('
'); + $('.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); + }, + }); + } + }); + +} \ No newline at end of file diff --git a/src/js/leaflet.draw.js b/src/js/leaflet.draw.js new file mode 100644 index 0000000..28f9338 --- /dev/null +++ b/src/js/leaflet.draw.js @@ -0,0 +1,10 @@ +/* + Leaflet.draw 0.4.14, a plugin that adds drawing and editing tools to Leaflet powered maps. + (c) 2012-2017, Jacob Toye, Jon West, Smartrak, Leaflet + + https://github.com/Leaflet/Leaflet.draw + http://leafletjs.com + */ +!function(t,e,i){function o(t,e){for(;(t=t.parentElement)&&!t.classList.contains(e););return t}L.drawVersion="0.4.14",L.Draw={},L.drawLocal={draw:{toolbar:{actions:{title:"Cancel drawing",text:"Cancel"},finish:{title:"Finish drawing",text:"Finish"},undo:{title:"Delete last point drawn",text:"Delete last point"},buttons:{polyline:"Draw a polyline",polygon:"Draw a polygon",rectangle:"Draw a rectangle",circle:"Draw a circle",marker:"Draw a marker",circlemarker:"Draw a circlemarker"}},handlers:{circle:{tooltip:{start:"Click and drag to draw circle."},radius:"Radius"},circlemarker:{tooltip:{start:"Click map to place circle marker."}},marker:{tooltip:{start:"Click map to place marker."}},polygon:{tooltip:{start:"Click to start drawing shape.",cont:"Click to continue drawing shape.",end:"Click first point to close this shape."}},polyline:{error:"Error: shape edges cannot cross!",tooltip:{start:"Click to start drawing line.",cont:"Click to continue drawing line.",end:"Click last point to finish line."}},rectangle:{tooltip:{start:"Click and drag to draw rectangle."}},simpleshape:{tooltip:{end:"Release mouse to finish drawing."}}}},edit:{toolbar:{actions:{save:{title:"Save changes",text:"Save"},cancel:{title:"Cancel editing, discards all changes",text:"Cancel"},clearAll:{title:"Clear all layers",text:"Clear All"}},buttons:{edit:"Edit layers",editDisabled:"No layers to edit",remove:"Delete layers",removeDisabled:"No layers to delete"}},handlers:{edit:{tooltip:{text:"Drag handles or markers to edit features.",subtext:"Click cancel to undo changes."}},remove:{tooltip:{text:"Click on a feature to remove."}}}}},L.Draw.Event={},L.Draw.Event.CREATED="draw:created",L.Draw.Event.EDITED="draw:edited",L.Draw.Event.DELETED="draw:deleted",L.Draw.Event.DRAWSTART="draw:drawstart",L.Draw.Event.DRAWSTOP="draw:drawstop",L.Draw.Event.DRAWVERTEX="draw:drawvertex",L.Draw.Event.EDITSTART="draw:editstart",L.Draw.Event.EDITMOVE="draw:editmove",L.Draw.Event.EDITRESIZE="draw:editresize",L.Draw.Event.EDITVERTEX="draw:editvertex",L.Draw.Event.EDITSTOP="draw:editstop",L.Draw.Event.DELETESTART="draw:deletestart",L.Draw.Event.DELETESTOP="draw:deletestop",L.Draw.Event.TOOLBAROPENED="draw:toolbaropened",L.Draw.Event.TOOLBARCLOSED="draw:toolbarclosed",L.Draw.Event.MARKERCONTEXT="draw:markercontext",L.Draw=L.Draw||{},L.Draw.Feature=L.Handler.extend({initialize:function(t,e){this._map=t,this._container=t._container,this._overlayPane=t._panes.overlayPane,this._popupPane=t._panes.popupPane,e&&e.shapeOptions&&(e.shapeOptions=L.Util.extend({},this.options.shapeOptions,e.shapeOptions)),L.setOptions(this,e);var i=L.version.split(".");1===parseInt(i[0],10)&&parseInt(i[1],10)>=2?L.Draw.Feature.include(L.Evented.prototype):L.Draw.Feature.include(L.Mixin.Events)},enable:function(){this._enabled||(L.Handler.prototype.enable.call(this),this.fire("enabled",{handler:this.type}),this._map.fire(L.Draw.Event.DRAWSTART,{layerType:this.type}))},disable:function(){this._enabled&&(L.Handler.prototype.disable.call(this),this._map.fire(L.Draw.Event.DRAWSTOP,{layerType:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(L.DomUtil.disableTextSelection(),t.getContainer().focus(),this._tooltip=new L.Draw.Tooltip(this._map),L.DomEvent.on(this._container,"keyup",this._cancelDrawing,this))},removeHooks:function(){this._map&&(L.DomUtil.enableTextSelection(),this._tooltip.dispose(),this._tooltip=null,L.DomEvent.off(this._container,"keyup",this._cancelDrawing,this))},setOptions:function(t){L.setOptions(this,t)},_fireCreatedEvent:function(t){this._map.fire(L.Draw.Event.CREATED,{layer:t,layerType:this.type})},_cancelDrawing:function(t){27===t.keyCode&&(this._map.fire("draw:canceled",{layerType:this.type}),this.disable())}}),L.Draw.Polyline=L.Draw.Feature.extend({statics:{TYPE:"polyline"},Poly:L.Polyline,options:{allowIntersection:!0,repeatMode:!1,drawError:{color:"#b00b00",timeout:2500},icon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon"}),touchIcon:new L.DivIcon({iconSize:new L.Point(20,20),className:"leaflet-div-icon leaflet-editing-icon leaflet-touch-icon"}),guidelineDistance:20,maxGuideLineLength:4e3,shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!1,clickable:!0},metric:!0,feet:!0,nautic:!1,showLength:!0,zIndexOffset:2e3,factor:1,maxPoints:0},initialize:function(t,e){L.Browser.touch&&(this.options.icon=this.options.touchIcon),this.options.drawError.message=L.drawLocal.draw.handlers.polyline.error,e&&e.drawError&&(e.drawError=L.Util.extend({},this.options.drawError,e.drawError)),this.type=L.Draw.Polyline.TYPE,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._markers=[],this._markerGroup=new L.LayerGroup,this._map.addLayer(this._markerGroup),this._poly=new L.Polyline([],this.options.shapeOptions),this._tooltip.updateContent(this._getTooltipText()),this._mouseMarker||(this._mouseMarker=L.marker(this._map.getCenter(),{icon:L.divIcon({className:"leaflet-mouse-marker",iconAnchor:[20,20],iconSize:[40,40]}),opacity:0,zIndexOffset:this.options.zIndexOffset})),this._mouseMarker.on("mouseout",this._onMouseOut,this).on("mousemove",this._onMouseMove,this).on("mousedown",this._onMouseDown,this).on("mouseup",this._onMouseUp,this).addTo(this._map),this._map.on("mouseup",this._onMouseUp,this).on("mousemove",this._onMouseMove,this).on("zoomlevelschange",this._onZoomEnd,this).on("touchstart",this._onTouch,this).on("zoomend",this._onZoomEnd,this))},removeHooks:function(){L.Draw.Feature.prototype.removeHooks.call(this),this._clearHideErrorTimeout(),this._cleanUpShape(),this._map.removeLayer(this._markerGroup),delete this._markerGroup,delete this._markers,this._map.removeLayer(this._poly),delete this._poly,this._mouseMarker.off("mousedown",this._onMouseDown,this).off("mouseout",this._onMouseOut,this).off("mouseup",this._onMouseUp,this).off("mousemove",this._onMouseMove,this),this._map.removeLayer(this._mouseMarker),delete this._mouseMarker,this._clearGuides(),this._map.off("mouseup",this._onMouseUp,this).off("mousemove",this._onMouseMove,this).off("zoomlevelschange",this._onZoomEnd,this).off("zoomend",this._onZoomEnd,this).off("touchstart",this._onTouch,this).off("click",this._onTouch,this)},deleteLastVertex:function(){if(!(this._markers.length<=1)){var t=this._markers.pop(),e=this._poly,i=e.getLatLngs(),o=i.splice(-1,1)[0];this._poly.setLatLngs(i),this._markerGroup.removeLayer(t),e.getLatLngs().length<2&&this._map.removeLayer(e),this._vertexChanged(o,!1)}},addVertex:function(t){if(this._markers.length>=2&&!this.options.allowIntersection&&this._poly.newLatLngIntersects(t))return void this._showErrorTooltip();this._errorShown&&this._hideErrorTooltip(),this._markers.push(this._createMarker(t)),this._poly.addLatLng(t),2===this._poly.getLatLngs().length&&this._map.addLayer(this._poly),this._vertexChanged(t,!0)},completeShape:function(){this._markers.length<=1||(this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable())},_finishShape:function(){var t=this._poly._defaultShape?this._poly._defaultShape():this._poly.getLatLngs(),e=this._poly.newLatLngIntersects(t[t.length-1]);if(!this.options.allowIntersection&&e||!this._shapeIsValid())return void this._showErrorTooltip();this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable()},_shapeIsValid:function(){return!0},_onZoomEnd:function(){null!==this._markers&&this._updateGuide()},_onMouseMove:function(t){var e=this._map.mouseEventToLayerPoint(t.originalEvent),i=this._map.layerPointToLatLng(e);this._currentLatLng=i,this._updateTooltip(i),this._updateGuide(e),this._mouseMarker.setLatLng(i),L.DomEvent.preventDefault(t.originalEvent)},_vertexChanged:function(t,e){this._map.fire(L.Draw.Event.DRAWVERTEX,{layers:this._markerGroup}),this._updateFinishHandler(),this._updateRunningMeasure(t,e),this._clearGuides(),this._updateTooltip()},_onMouseDown:function(t){if(!this._clickHandled&&!this._touchHandled&&!this._disableMarkers){this._onMouseMove(t),this._clickHandled=!0,this._disableNewMarkers();var e=t.originalEvent,i=e.clientX,o=e.clientY;this._startPoint.call(this,i,o)}},_startPoint:function(t,e){this._mouseDownOrigin=L.point(t,e)},_onMouseUp:function(t){var e=t.originalEvent,i=e.clientX,o=e.clientY;this._endPoint.call(this,i,o,t),this._clickHandled=null},_endPoint:function(e,i,o){if(this._mouseDownOrigin){var n=L.point(e,i).distanceTo(this._mouseDownOrigin),a=this._calculateFinishDistance(o.latlng);this.options.maxPoints>1&&this.options.maxPoints==this._markers.length+1?(this.addVertex(o.latlng),this._finishShape()):a<10&&L.Browser.touch?this._finishShape():Math.abs(n)<9*(t.devicePixelRatio||1)&&this.addVertex(o.latlng),this._enableNewMarkers()}this._mouseDownOrigin=null},_onTouch:function(t){var e,i,o=t.originalEvent;!o.touches||!o.touches[0]||this._clickHandled||this._touchHandled||this._disableMarkers||(e=o.touches[0].clientX,i=o.touches[0].clientY,this._disableNewMarkers(),this._touchHandled=!0,this._startPoint.call(this,e,i),this._endPoint.call(this,e,i,t),this._touchHandled=null),this._clickHandled=null},_onMouseOut:function(){this._tooltip&&this._tooltip._onMouseOut.call(this._tooltip)},_calculateFinishDistance:function(t){var e;if(this._markers.length>0){var i;if(this.type===L.Draw.Polyline.TYPE)i=this._markers[this._markers.length-1];else{if(this.type!==L.Draw.Polygon.TYPE)return 1/0;i=this._markers[0]}var o=this._map.latLngToContainerPoint(i.getLatLng()),n=new L.Marker(t,{icon:this.options.icon,zIndexOffset:2*this.options.zIndexOffset}),a=this._map.latLngToContainerPoint(n.getLatLng());e=o.distanceTo(a)}else e=1/0;return e},_updateFinishHandler:function(){var t=this._markers.length;t>1&&this._markers[t-1].on("click",this._finishShape,this),t>2&&this._markers[t-2].off("click",this._finishShape,this)},_createMarker:function(t){var e=new L.Marker(t,{icon:this.options.icon,zIndexOffset:2*this.options.zIndexOffset});return this._markerGroup.addLayer(e),e},_updateGuide:function(t){var e=this._markers?this._markers.length:0;e>0&&(t=t||this._map.latLngToLayerPoint(this._currentLatLng),this._clearGuides(),this._drawGuide(this._map.latLngToLayerPoint(this._markers[e-1].getLatLng()),t))},_updateTooltip:function(t){var e=this._getTooltipText();t&&this._tooltip.updatePosition(t),this._errorShown||this._tooltip.updateContent(e)},_drawGuide:function(t,e){var i,o,n,a=Math.floor(Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))),s=this.options.guidelineDistance,r=this.options.maxGuideLineLength,l=a>r?a-r:s;for(this._guidesContainer||(this._guidesContainer=L.DomUtil.create("div","leaflet-draw-guides",this._overlayPane));l1&&this._markers[this._markers.length-1].off("click",this._finishShape,this)},_fireCreatedEvent:function(){var t=new this.Poly(this._poly.getLatLngs(),this.options.shapeOptions);L.Draw.Feature.prototype._fireCreatedEvent.call(this,t)}}),L.Draw.Polygon=L.Draw.Polyline.extend({statics:{TYPE:"polygon"},Poly:L.Polygon,options:{showArea:!1,showLength:!1,shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0},metric:!0,feet:!0,nautic:!1,precision:{}},initialize:function(t,e){L.Draw.Polyline.prototype.initialize.call(this,t,e),this.type=L.Draw.Polygon.TYPE},_updateFinishHandler:function(){var t=this._markers.length;1===t&&this._markers[0].on("click",this._finishShape,this),t>2&&(this._markers[t-1].on("dblclick",this._finishShape,this),t>3&&this._markers[t-2].off("dblclick",this._finishShape,this))},_getTooltipText:function(){var t,e;return 0===this._markers.length?t=L.drawLocal.draw.handlers.polygon.tooltip.start:this._markers.length<3?(t=L.drawLocal.draw.handlers.polygon.tooltip.cont,e=this._getMeasurementString()):(t=L.drawLocal.draw.handlers.polygon.tooltip.end,e=this._getMeasurementString()),{text:t,subtext:e}},_getMeasurementString:function(){var t=this._area,e="";return t||this.options.showLength?(this.options.showLength&&(e=L.Draw.Polyline.prototype._getMeasurementString.call(this)),t&&(e+="
"+L.GeometryUtil.readableArea(t,this.options.metric,this.options.precision)),e):null},_shapeIsValid:function(){return this._markers.length>=3},_vertexChanged:function(t,e){var i;!this.options.allowIntersection&&this.options.showArea&&(i=this._poly.getLatLngs(),this._area=L.GeometryUtil.geodesicArea(i)),L.Draw.Polyline.prototype._vertexChanged.call(this,t,e)},_cleanUpShape:function(){var t=this._markers.length;t>0&&(this._markers[0].off("click",this._finishShape,this),t>2&&this._markers[t-1].off("dblclick",this._finishShape,this))}}),L.SimpleShape={},L.Draw.SimpleShape=L.Draw.Feature.extend({options:{repeatMode:!1},initialize:function(t,e){this._endLabelText=L.drawLocal.draw.handlers.simpleshape.tooltip.end,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._mapDraggable=this._map.dragging.enabled(),this._mapDraggable&&this._map.dragging.disable(),this._container.style.cursor="crosshair",this._tooltip.updateContent({text:this._initialLabelText}),this._map.on("mousedown",this._onMouseDown,this).on("mousemove",this._onMouseMove,this).on("touchstart",this._onMouseDown,this).on("touchmove",this._onMouseMove,this),e.addEventListener("touchstart",L.DomEvent.preventDefault,{passive:!1}))},removeHooks:function(){L.Draw.Feature.prototype.removeHooks.call(this),this._map&&(this._mapDraggable&&this._map.dragging.enable(),this._container.style.cursor="",this._map.off("mousedown",this._onMouseDown,this).off("mousemove",this._onMouseMove,this).off("touchstart",this._onMouseDown,this).off("touchmove",this._onMouseMove,this),L.DomEvent.off(e,"mouseup",this._onMouseUp,this),L.DomEvent.off(e,"touchend",this._onMouseUp,this),e.removeEventListener("touchstart",L.DomEvent.preventDefault),this._shape&&(this._map.removeLayer(this._shape),delete this._shape)),this._isDrawing=!1},_getTooltipText:function(){return{text:this._endLabelText}},_onMouseDown:function(t){this._isDrawing=!0,this._startLatLng=t.latlng,L.DomEvent.on(e,"mouseup",this._onMouseUp,this).on(e,"touchend",this._onMouseUp,this).preventDefault(t.originalEvent)},_onMouseMove:function(t){var e=t.latlng;this._tooltip.updatePosition(e),this._isDrawing&&(this._tooltip.updateContent(this._getTooltipText()),this._drawShape(e))},_onMouseUp:function(){this._shape&&this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable()}}),L.Draw.Rectangle=L.Draw.SimpleShape.extend({statics:{TYPE:"rectangle"},options:{shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,showArea:!0,clickable:!0},metric:!0},initialize:function(t,e){this.type=L.Draw.Rectangle.TYPE,this._initialLabelText=L.drawLocal.draw.handlers.rectangle.tooltip.start,L.Draw.SimpleShape.prototype.initialize.call(this,t,e)},disable:function(){this._enabled&&(this._isCurrentlyTwoClickDrawing=!1,L.Draw.SimpleShape.prototype.disable.call(this))},_onMouseUp:function(t){if(!this._shape&&!this._isCurrentlyTwoClickDrawing)return void(this._isCurrentlyTwoClickDrawing=!0);this._isCurrentlyTwoClickDrawing&&!o(t.target,"leaflet-pane")||L.Draw.SimpleShape.prototype._onMouseUp.call(this)},_drawShape:function(t){this._shape?this._shape.setBounds(new L.LatLngBounds(this._startLatLng,t)):(this._shape=new L.Rectangle(new L.LatLngBounds(this._startLatLng,t),this.options.shapeOptions),this._map.addLayer(this._shape))},_fireCreatedEvent:function(){var t=new L.Rectangle(this._shape.getBounds(),this.options.shapeOptions);L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this,t)},_getTooltipText:function(){var t,e,i,o=L.Draw.SimpleShape.prototype._getTooltipText.call(this),n=this._shape,a=this.options.showArea;return n&&(t=this._shape._defaultShape?this._shape._defaultShape():this._shape.getLatLngs(),e=L.GeometryUtil.geodesicArea(t),i=a?L.GeometryUtil.readableArea(e,this.options.metric):""),{text:o.text,subtext:i}}}),L.Draw.Marker=L.Draw.Feature.extend({statics:{TYPE:"marker"},options:{icon:new L.Icon.Default,repeatMode:!1,zIndexOffset:2e3},initialize:function(t,e){this.type=L.Draw.Marker.TYPE,this._initialLabelText=L.drawLocal.draw.handlers.marker.tooltip.start,L.Draw.Feature.prototype.initialize.call(this,t,e)},addHooks:function(){L.Draw.Feature.prototype.addHooks.call(this),this._map&&(this._tooltip.updateContent({text:this._initialLabelText}),this._mouseMarker||(this._mouseMarker=L.marker(this._map.getCenter(),{icon:L.divIcon({className:"leaflet-mouse-marker",iconAnchor:[20,20],iconSize:[40,40]}),opacity:0,zIndexOffset:this.options.zIndexOffset})),this._mouseMarker.on("click",this._onClick,this).addTo(this._map),this._map.on("mousemove",this._onMouseMove,this),this._map.on("click",this._onTouch,this))},removeHooks:function(){L.Draw.Feature.prototype.removeHooks.call(this),this._map&&(this._map.off("click",this._onClick,this).off("click",this._onTouch,this),this._marker&&(this._marker.off("click",this._onClick,this),this._map.removeLayer(this._marker),delete this._marker),this._mouseMarker.off("click",this._onClick,this),this._map.removeLayer(this._mouseMarker),delete this._mouseMarker,this._map.off("mousemove",this._onMouseMove,this))},_onMouseMove:function(t){var e=t.latlng;this._tooltip.updatePosition(e),this._mouseMarker.setLatLng(e),this._marker?(e=this._mouseMarker.getLatLng(),this._marker.setLatLng(e)):(this._marker=this._createMarker(e),this._marker.on("click",this._onClick,this),this._map.on("click",this._onClick,this).addLayer(this._marker))},_createMarker:function(t){return new L.Marker(t,{icon:this.options.icon,zIndexOffset:this.options.zIndexOffset})},_onClick:function(){this._fireCreatedEvent(),this.disable(),this.options.repeatMode&&this.enable()},_onTouch:function(t){this._onMouseMove(t),this._onClick()},_fireCreatedEvent:function(){var t=new L.Marker.Touch(this._marker.getLatLng(),{icon:this.options.icon});L.Draw.Feature.prototype._fireCreatedEvent.call(this,t)}}),L.Draw.CircleMarker=L.Draw.Marker.extend({statics:{TYPE:"circlemarker"},options:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0,zIndexOffset:2e3},initialize:function(t,e){this.type=L.Draw.CircleMarker.TYPE,this._initialLabelText=L.drawLocal.draw.handlers.circlemarker.tooltip.start,L.Draw.Feature.prototype.initialize.call(this,t,e)},_fireCreatedEvent:function(){var t=new L.CircleMarker(this._marker.getLatLng(),this.options);L.Draw.Feature.prototype._fireCreatedEvent.call(this,t)},_createMarker:function(t){return new L.CircleMarker(t,this.options)}}),L.Draw.Circle=L.Draw.SimpleShape.extend({statics:{TYPE:"circle"},options:{shapeOptions:{stroke:!0,color:"#3388ff",weight:4,opacity:.5,fill:!0,fillColor:null,fillOpacity:.2,clickable:!0},showRadius:!0,metric:!0,feet:!0,nautic:!1},initialize:function(t,e){this.type=L.Draw.Circle.TYPE,this._initialLabelText=L.drawLocal.draw.handlers.circle.tooltip.start,L.Draw.SimpleShape.prototype.initialize.call(this,t,e)},_drawShape:function(t){if(L.GeometryUtil.isVersion07x())var e=this._startLatLng.distanceTo(t);else var e=this._map.distance(this._startLatLng,t);this._shape?this._shape.setRadius(e):(this._shape=new L.Circle(this._startLatLng,e,this.options.shapeOptions),this._map.addLayer(this._shape))},_fireCreatedEvent:function(){var t=new L.Circle(this._startLatLng,this._shape.getRadius(),this.options.shapeOptions);L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this,t)},_onMouseMove:function(t){var e,i=t.latlng,o=this.options.showRadius,n=this.options.metric;if(this._tooltip.updatePosition(i),this._isDrawing){this._drawShape(i),e=this._shape.getRadius().toFixed(1);var a="";o&&(a=L.drawLocal.draw.handlers.circle.radius+": "+L.GeometryUtil.readableDistance(e,n,this.options.feet,this.options.nautic)),this._tooltip.updateContent({text:this._endLabelText,subtext:a})}}}),L.Edit=L.Edit||{},L.Edit.Marker=L.Handler.extend({initialize:function(t,e){this._marker=t,L.setOptions(this,e)},addHooks:function(){var t=this._marker;t.dragging.enable(),t.on("dragend",this._onDragEnd,t),this._toggleMarkerHighlight()},removeHooks:function(){var t=this._marker;t.dragging.disable(),t.off("dragend",this._onDragEnd,t),this._toggleMarkerHighlight()},_onDragEnd:function(t){var e=t.target;e.edited=!0,this._map.fire(L.Draw.Event.EDITMOVE,{layer:e})},_toggleMarkerHighlight:function(){var t=this._marker._icon;t&&(t.style.display="none",L.DomUtil.hasClass(t,"leaflet-edit-marker-selected")?(L.DomUtil.removeClass(t,"leaflet-edit-marker-selected"),this._offsetMarker(t,-4)):(L.DomUtil.addClass(t,"leaflet-edit-marker-selected"),this._offsetMarker(t,4)),t.style.display="")},_offsetMarker:function(t,e){var i=parseInt(t.style.marginTop,10)-e,o=parseInt(t.style.marginLeft,10)-e;t.style.marginTop=i+"px",t.style.marginLeft=o+"px"}}),L.Marker.addInitHook(function(){L.Edit.Marker&&(this.editing=new L.Edit.Marker(this),this.options.editable&&this.editing.enable())}),L.Edit=L.Edit||{},L.Edit.Poly=L.Handler.extend({initialize:function(t){this.latlngs=[t._latlngs],t._holes&&(this.latlngs=this.latlngs.concat(t._holes)),this._poly=t,this._poly.on("revert-edited",this._updateLatLngs,this)},_defaultShape:function(){return L.Polyline._flat?L.Polyline._flat(this._poly._latlngs)?this._poly._latlngs:this._poly._latlngs[0]:this._poly._latlngs},_eachVertexHandler:function(t){for(var e=0;et&&(i._index+=e)})},_createMiddleMarker:function(t,e){var i,o,n,a=this._getMiddleLatLng(t,e),s=this._createMarker(a);s.setOpacity(.6),t._middleRight=e._middleLeft=s,o=function(){s.off("touchmove",o,this);var n=e._index;s._index=n,s.off("click",i,this).on("click",this._onMarkerClick,this),a.lat=s.getLatLng().lat,a.lng=s.getLatLng().lng,this._spliceLatLngs(n,0,a),this._markers.splice(n,0,s),s.setOpacity(1),this._updateIndexes(n,1),e._index++,this._updatePrevNext(t,s),this._updatePrevNext(s,e),this._poly.fire("editstart")},n=function(){s.off("dragstart",o,this),s.off("dragend",n,this),s.off("touchmove",o,this),this._createMiddleMarker(t,s),this._createMiddleMarker(s,e)},i=function(){o.call(this),n.call(this),this._fireEdit()},s.on("click",i,this).on("dragstart",o,this).on("dragend",n,this).on("touchmove",o,this),this._markerGroup.addLayer(s)},_updatePrevNext:function(t,e){t&&(t._next=e),e&&(e._prev=t)},_getMiddleLatLng:function(t,e){var i=this._poly._map,o=i.project(t.getLatLng()),n=i.project(e.getLatLng());return i.unproject(o._add(n)._divideBy(2))}}),L.Polyline.addInitHook(function(){this.editing||(L.Edit.Poly&&(this.editing=new L.Edit.Poly(this),this.options.editable&&this.editing.enable()),this.on("add",function(){this.editing&&this.editing.enabled()&&this.editing.addHooks()}),this.on("remove",function(){this.editing&&this.editing.enabled()&&this.editing.removeHooks()}))}),L.Edit=L.Edit||{},L.Edit.SimpleShape=L.Handler.extend({options:{moveIcon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-move"}),resizeIcon:new L.DivIcon({iconSize:new L.Point(8,8),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-resize"}),touchMoveIcon:new L.DivIcon({ +iconSize:new L.Point(20,20),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-move leaflet-touch-icon"}),touchResizeIcon:new L.DivIcon({iconSize:new L.Point(20,20),className:"leaflet-div-icon leaflet-editing-icon leaflet-edit-resize leaflet-touch-icon"})},initialize:function(t,e){L.Browser.touch&&(this.options.moveIcon=this.options.touchMoveIcon,this.options.resizeIcon=this.options.touchResizeIcon),this._shape=t,L.Util.setOptions(this,e)},addHooks:function(){var t=this._shape;this._shape._map&&(this._map=this._shape._map,t.setStyle(t.options.editing),t._map&&(this._map=t._map,this._markerGroup||this._initMarkers(),this._map.addLayer(this._markerGroup)))},removeHooks:function(){var t=this._shape;if(t.setStyle(t.options.original),t._map){this._unbindMarker(this._moveMarker);for(var e=0,i=this._resizeMarkers.length;e"+L.drawLocal.edit.handlers.edit.tooltip.text,subtext:L.drawLocal.draw.handlers.circle.radius+": "+L.GeometryUtil.readableDistance(radius,!0,this.options.feet,this.options.nautic)}),this._shape.setRadius(radius),this._map.fire(L.Draw.Event.EDITRESIZE,{layer:this._shape})}}),L.Circle.addInitHook(function(){L.Edit.Circle&&(this.editing=new L.Edit.Circle(this),this.options.editable&&this.editing.enable()),this.on("add",function(){this.editing&&this.editing.enabled()&&this.editing.addHooks()}),this.on("remove",function(){this.editing&&this.editing.enabled()&&this.editing.removeHooks()})}),L.Map.mergeOptions({touchExtend:!0}),L.Map.TouchExtend=L.Handler.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane},addHooks:function(){L.DomEvent.on(this._container,"touchstart",this._onTouchStart,this),L.DomEvent.on(this._container,"touchend",this._onTouchEnd,this),L.DomEvent.on(this._container,"touchmove",this._onTouchMove,this),this._detectIE()?(L.DomEvent.on(this._container,"MSPointerDown",this._onTouchStart,this),L.DomEvent.on(this._container,"MSPointerUp",this._onTouchEnd,this),L.DomEvent.on(this._container,"MSPointerMove",this._onTouchMove,this),L.DomEvent.on(this._container,"MSPointerCancel",this._onTouchCancel,this)):(L.DomEvent.on(this._container,"touchcancel",this._onTouchCancel,this),L.DomEvent.on(this._container,"touchleave",this._onTouchLeave,this))},removeHooks:function(){L.DomEvent.off(this._container,"touchstart",this._onTouchStart),L.DomEvent.off(this._container,"touchend",this._onTouchEnd),L.DomEvent.off(this._container,"touchmove",this._onTouchMove),this._detectIE()?(L.DomEvent.off(this._container,"MSPointerDowm",this._onTouchStart),L.DomEvent.off(this._container,"MSPointerUp",this._onTouchEnd),L.DomEvent.off(this._container,"MSPointerMove",this._onTouchMove),L.DomEvent.off(this._container,"MSPointerCancel",this._onTouchCancel)):(L.DomEvent.off(this._container,"touchcancel",this._onTouchCancel),L.DomEvent.off(this._container,"touchleave",this._onTouchLeave))},_touchEvent:function(t,e){var i={};if(void 0!==t.touches){if(!t.touches.length)return;i=t.touches[0]}else{if("touch"!==t.pointerType)return;if(i=t,!this._filterClick(t))return}var o=this._map.mouseEventToContainerPoint(i),n=this._map.mouseEventToLayerPoint(i),a=this._map.layerPointToLatLng(n);this._map.fire(e,{latlng:a,layerPoint:n,containerPoint:o,pageX:i.pageX,pageY:i.pageY,originalEvent:t})},_filterClick:function(t){var e=t.timeStamp||t.originalEvent.timeStamp,i=L.DomEvent._lastClick&&e-L.DomEvent._lastClick;return i&&i>100&&i<500||t.target._simulatedClick&&!t._simulated?(L.DomEvent.stop(t),!1):(L.DomEvent._lastClick=e,!0)},_onTouchStart:function(t){if(this._map._loaded){this._touchEvent(t,"touchstart")}},_onTouchEnd:function(t){if(this._map._loaded){this._touchEvent(t,"touchend")}},_onTouchCancel:function(t){if(this._map._loaded){var e="touchcancel";this._detectIE()&&(e="pointercancel"),this._touchEvent(t,e)}},_onTouchLeave:function(t){if(this._map._loaded){this._touchEvent(t,"touchleave")}},_onTouchMove:function(t){if(this._map._loaded){this._touchEvent(t,"touchmove")}},_detectIE:function(){var e=t.navigator.userAgent,i=e.indexOf("MSIE ");if(i>0)return parseInt(e.substring(i+5,e.indexOf(".",i)),10);if(e.indexOf("Trident/")>0){var o=e.indexOf("rv:");return parseInt(e.substring(o+3,e.indexOf(".",o)),10)}var n=e.indexOf("Edge/");return n>0&&parseInt(e.substring(n+5,e.indexOf(".",n)),10)}}),L.Map.addInitHook("addHandler","touchExtend",L.Map.TouchExtend),L.Marker.Touch=L.Marker.extend({_initInteraction:function(){return this.addInteractiveTarget?L.Marker.prototype._initInteraction.apply(this):this._initInteractionLegacy()},_initInteractionLegacy:function(){if(this.options.clickable){var t=this._icon,e=["dblclick","mousedown","mouseover","mouseout","contextmenu","touchstart","touchend","touchmove"];this._detectIE?e.concat(["MSPointerDown","MSPointerUp","MSPointerMove","MSPointerCancel"]):e.concat(["touchcancel"]),L.DomUtil.addClass(t,"leaflet-clickable"),L.DomEvent.on(t,"click",this._onMouseClick,this),L.DomEvent.on(t,"keypress",this._onKeyPress,this);for(var i=0;i0)return parseInt(e.substring(i+5,e.indexOf(".",i)),10);if(e.indexOf("Trident/")>0){var o=e.indexOf("rv:");return parseInt(e.substring(o+3,e.indexOf(".",o)),10)}var n=e.indexOf("Edge/");return n>0&&parseInt(e.substring(n+5,e.indexOf(".",n)),10)}}),L.LatLngUtil={cloneLatLngs:function(t){for(var e=[],i=0,o=t.length;i2){for(var s=0;s1&&(i=i+s+r[1])}return i},readableArea:function(e,i,o){var n,a,o=L.Util.extend({},t,o);return i?(a=["ha","m"],type=typeof i,"string"===type?a=[i]:"boolean"!==type&&(a=i),n=e>=1e6&&-1!==a.indexOf("km")?L.GeometryUtil.formattedNumber(1e-6*e,o.km)+" km²":e>=1e4&&-1!==a.indexOf("ha")?L.GeometryUtil.formattedNumber(1e-4*e,o.ha)+" ha":L.GeometryUtil.formattedNumber(e,o.m)+" m²"):(e/=.836127,n=e>=3097600?L.GeometryUtil.formattedNumber(e/3097600,o.mi)+" mi²":e>=4840?L.GeometryUtil.formattedNumber(e/4840,o.ac)+" acres":L.GeometryUtil.formattedNumber(e,o.yd)+" yd²"),n},readableDistance:function(e,i,o,n,a){var s,a=L.Util.extend({},t,a);switch(i?"string"==typeof i?i:"metric":o?"feet":n?"nauticalMile":"yards"){case"metric":s=e>1e3?L.GeometryUtil.formattedNumber(e/1e3,a.km)+" km":L.GeometryUtil.formattedNumber(e,a.m)+" m";break;case"feet":e*=3.28083,s=L.GeometryUtil.formattedNumber(e,a.ft)+" ft";break;case"nauticalMile":e*=.53996,s=L.GeometryUtil.formattedNumber(e/1e3,a.nm)+" nm";break;case"yards":default:e*=1.09361,s=e>1760?L.GeometryUtil.formattedNumber(e/1760,a.mi)+" miles":L.GeometryUtil.formattedNumber(e,a.yd)+" yd"}return s},isVersion07x:function(){var t=L.version.split(".");return 0===parseInt(t[0],10)&&7===parseInt(t[1],10)}})}(),L.Util.extend(L.LineUtil,{segmentsIntersect:function(t,e,i,o){return this._checkCounterclockwise(t,i,o)!==this._checkCounterclockwise(e,i,o)&&this._checkCounterclockwise(t,e,i)!==this._checkCounterclockwise(t,e,o)},_checkCounterclockwise:function(t,e,i){return(i.y-t.y)*(e.x-t.x)>(e.y-t.y)*(i.x-t.x)}}),L.Polyline.include({intersects:function(){var t,e,i,o=this._getProjectedPoints(),n=o?o.length:0;if(this._tooFewPointsForIntersection())return!1;for(t=n-1;t>=3;t--)if(e=o[t-1],i=o[t],this._lineSegmentsIntersectsRange(e,i,t-2))return!0;return!1},newLatLngIntersects:function(t,e){return!!this._map&&this.newPointIntersects(this._map.latLngToLayerPoint(t),e)},newPointIntersects:function(t,e){var i=this._getProjectedPoints(),o=i?i.length:0,n=i?i[o-1]:null,a=o-2;return!this._tooFewPointsForIntersection(1)&&this._lineSegmentsIntersectsRange(n,t,a,e?1:0)},_tooFewPointsForIntersection:function(t){var e=this._getProjectedPoints(),i=e?e.length:0;return i+=t||0,!e||i<=3},_lineSegmentsIntersectsRange:function(t,e,i,o){var n,a,s=this._getProjectedPoints();o=o||0;for(var r=i;r>o;r--)if(n=s[r-1],a=s[r],L.LineUtil.segmentsIntersect(t,e,n,a))return!0;return!1},_getProjectedPoints:function(){if(!this._defaultShape)return this._originalPoints;for(var t=[],e=this._defaultShape(),i=0;i=2?L.Toolbar.include(L.Evented.prototype):L.Toolbar.include(L.Mixin.Events)},enabled:function(){return null!==this._activeMode},disable:function(){this.enabled()&&this._activeMode.handler.disable()},addToolbar:function(t){var e,i=L.DomUtil.create("div","leaflet-draw-section"),o=0,n=this._toolbarClass||"",a=this.getModeHandlers(t);for(this._toolbarContainer=L.DomUtil.create("div","leaflet-draw-toolbar leaflet-bar"),this._map=t,e=0;e0&&this._singleLineLabel&&(L.DomUtil.removeClass(this._container,"leaflet-draw-tooltip-single"),this._singleLineLabel=!1):(L.DomUtil.addClass(this._container,"leaflet-draw-tooltip-single"),this._singleLineLabel=!0),this._container.innerHTML=(t.subtext.length>0?''+t.subtext+"
":"")+""+t.text+"",t.text||t.subtext?(this._visible=!0,this._container.style.visibility="inherit"):(this._visible=!1,this._container.style.visibility="hidden"),this):this},updatePosition:function(t){var e=this._map.latLngToLayerPoint(t),i=this._container;return this._container&&(this._visible&&(i.style.visibility="inherit"),L.DomUtil.setPosition(i,e)),this},showAsError:function(){return this._container&&L.DomUtil.addClass(this._container,"leaflet-error-draw-tooltip"),this},removeError:function(){return this._container&&L.DomUtil.removeClass(this._container,"leaflet-error-draw-tooltip"),this},_onMouseOut:function(){this._container&&(this._container.style.visibility="hidden")}}),L.DrawToolbar=L.Toolbar.extend({statics:{TYPE:"draw"},options:{polyline:{},polygon:{},rectangle:{},circle:{},marker:{},circlemarker:{}},initialize:function(t){for(var e in this.options)this.options.hasOwnProperty(e)&&t[e]&&(t[e]=L.extend({},this.options[e],t[e]));this._toolbarClass="leaflet-draw-draw",L.Toolbar.prototype.initialize.call(this,t)},getModeHandlers:function(t){return[{enabled:this.options.polyline,handler:new L.Draw.Polyline(t,this.options.polyline),title:L.drawLocal.draw.toolbar.buttons.polyline},{enabled:this.options.polygon,handler:new L.Draw.Polygon(t,this.options.polygon),title:L.drawLocal.draw.toolbar.buttons.polygon},{enabled:this.options.rectangle,handler:new L.Draw.Rectangle(t,this.options.rectangle),title:L.drawLocal.draw.toolbar.buttons.rectangle},{enabled:this.options.circle,handler:new L.Draw.Circle(t,this.options.circle),title:L.drawLocal.draw.toolbar.buttons.circle},{enabled:this.options.marker,handler:new L.Draw.Marker(t,this.options.marker),title:L.drawLocal.draw.toolbar.buttons.marker},{enabled:this.options.circlemarker,handler:new L.Draw.CircleMarker(t,this.options.circlemarker),title:L.drawLocal.draw.toolbar.buttons.circlemarker}]},getActions:function(t){return[{enabled:t.completeShape,title:L.drawLocal.draw.toolbar.finish.title,text:L.drawLocal.draw.toolbar.finish.text,callback:t.completeShape,context:t},{enabled:t.deleteLastVertex,title:L.drawLocal.draw.toolbar.undo.title,text:L.drawLocal.draw.toolbar.undo.text,callback:t.deleteLastVertex,context:t},{title:L.drawLocal.draw.toolbar.actions.title,text:L.drawLocal.draw.toolbar.actions.text,callback:this.disable,context:this}]},setOptions:function(t){L.setOptions(this,t);for(var e in this._modes)this._modes.hasOwnProperty(e)&&t.hasOwnProperty(e)&&this._modes[e].handler.setOptions(t[e])}}),L.EditToolbar=L.Toolbar.extend({statics:{TYPE:"edit"},options:{edit:{selectedPathOptions:{dashArray:"10, 10",fill:!0,fillColor:"#fe57a1",fillOpacity:.1,maintainColor:!1}},remove:{},poly:null,featureGroup:null},initialize:function(t){t.edit&&(void 0===t.edit.selectedPathOptions&&(t.edit.selectedPathOptions=this.options.edit.selectedPathOptions),t.edit.selectedPathOptions=L.extend({},this.options.edit.selectedPathOptions,t.edit.selectedPathOptions)),t.remove&&(t.remove=L.extend({},this.options.remove,t.remove)),t.poly&&(t.poly=L.extend({},this.options.poly,t.poly)),this._toolbarClass="leaflet-draw-edit",L.Toolbar.prototype.initialize.call(this,t),this._selectedFeatureCount=0},getModeHandlers:function(t){var e=this.options.featureGroup;return[{enabled:this.options.edit,handler:new L.EditToolbar.Edit(t,{featureGroup:e,selectedPathOptions:this.options.edit.selectedPathOptions,poly:this.options.poly}),title:L.drawLocal.edit.toolbar.buttons.edit},{enabled:this.options.remove,handler:new L.EditToolbar.Delete(t,{featureGroup:e}),title:L.drawLocal.edit.toolbar.buttons.remove}]},getActions:function(t){var e=[{title:L.drawLocal.edit.toolbar.actions.save.title,text:L.drawLocal.edit.toolbar.actions.save.text,callback:this._save,context:this},{title:L.drawLocal.edit.toolbar.actions.cancel.title,text:L.drawLocal.edit.toolbar.actions.cancel.text,callback:this.disable,context:this}];return t.removeAllLayers&&e.push({title:L.drawLocal.edit.toolbar.actions.clearAll.title,text:L.drawLocal.edit.toolbar.actions.clearAll.text,callback:this._clearAllLayers,context:this}),e},addToolbar:function(t){var e=L.Toolbar.prototype.addToolbar.call(this,t);return this._checkDisabled(),this.options.featureGroup.on("layeradd layerremove",this._checkDisabled,this),e},removeToolbar:function(){this.options.featureGroup.off("layeradd layerremove",this._checkDisabled,this),L.Toolbar.prototype.removeToolbar.call(this)},disable:function(){this.enabled()&&(this._activeMode.handler.revertLayers(),L.Toolbar.prototype.disable.call(this))},_save:function(){this._activeMode.handler.save(),this._activeMode&&this._activeMode.handler.disable()},_clearAllLayers:function(){this._activeMode.handler.removeAllLayers(),this._activeMode&&this._activeMode.handler.disable()},_checkDisabled:function(){var t,e=this.options.featureGroup,i=0!==e.getLayers().length;this.options.edit&&(t=this._modes[L.EditToolbar.Edit.TYPE].button,i?L.DomUtil.removeClass(t,"leaflet-disabled"):L.DomUtil.addClass(t,"leaflet-disabled"),t.setAttribute("title",i?L.drawLocal.edit.toolbar.buttons.edit:L.drawLocal.edit.toolbar.buttons.editDisabled)),this.options.remove&&(t=this._modes[L.EditToolbar.Delete.TYPE].button,i?L.DomUtil.removeClass(t,"leaflet-disabled"):L.DomUtil.addClass(t,"leaflet-disabled"),t.setAttribute("title",i?L.drawLocal.edit.toolbar.buttons.remove:L.drawLocal.edit.toolbar.buttons.removeDisabled))}}),L.EditToolbar.Edit=L.Handler.extend({statics:{TYPE:"edit"},initialize:function(t,e){if(L.Handler.prototype.initialize.call(this,t),L.setOptions(this,e),this._featureGroup=e.featureGroup,!(this._featureGroup instanceof L.FeatureGroup))throw new Error("options.featureGroup must be a L.FeatureGroup");this._uneditedLayerProps={},this.type=L.EditToolbar.Edit.TYPE;var i=L.version.split(".");1===parseInt(i[0],10)&&parseInt(i[1],10)>=2?L.EditToolbar.Edit.include(L.Evented.prototype):L.EditToolbar.Edit.include(L.Mixin.Events)},enable:function(){!this._enabled&&this._hasAvailableLayers()&&(this.fire("enabled",{handler:this.type}),this._map.fire(L.Draw.Event.EDITSTART,{handler:this.type}),L.Handler.prototype.enable.call(this),this._featureGroup.on("layeradd",this._enableLayerEdit,this).on("layerremove",this._disableLayerEdit,this))},disable:function(){this._enabled&&(this._featureGroup.off("layeradd",this._enableLayerEdit,this).off("layerremove",this._disableLayerEdit,this),L.Handler.prototype.disable.call(this),this._map.fire(L.Draw.Event.EDITSTOP,{handler:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(t.getContainer().focus(),this._featureGroup.eachLayer(this._enableLayerEdit,this),this._tooltip=new L.Draw.Tooltip(this._map),this._tooltip.updateContent({text:L.drawLocal.edit.handlers.edit.tooltip.text,subtext:L.drawLocal.edit.handlers.edit.tooltip.subtext}),t._editTooltip=this._tooltip,this._updateTooltip(),this._map.on("mousemove",this._onMouseMove,this).on("touchmove",this._onMouseMove,this).on("MSPointerMove",this._onMouseMove,this).on(L.Draw.Event.EDITVERTEX,this._updateTooltip,this))},removeHooks:function(){this._map&&(this._featureGroup.eachLayer(this._disableLayerEdit,this),this._uneditedLayerProps={},this._tooltip.dispose(),this._tooltip=null,this._map.off("mousemove",this._onMouseMove,this).off("touchmove",this._onMouseMove,this).off("MSPointerMove",this._onMouseMove,this).off(L.Draw.Event.EDITVERTEX,this._updateTooltip,this))},revertLayers:function(){this._featureGroup.eachLayer(function(t){this._revertLayer(t)},this)},save:function(){var t=new L.LayerGroup;this._featureGroup.eachLayer(function(e){e.edited&&(t.addLayer(e),e.edited=!1)}),this._map.fire(L.Draw.Event.EDITED,{layers:t})},_backupLayer:function(t){var e=L.Util.stamp(t);this._uneditedLayerProps[e]||(t instanceof L.Polyline||t instanceof L.Polygon||t instanceof L.Rectangle?this._uneditedLayerProps[e]={latlngs:L.LatLngUtil.cloneLatLngs(t.getLatLngs())}:t instanceof L.Circle?this._uneditedLayerProps[e]={latlng:L.LatLngUtil.cloneLatLng(t.getLatLng()),radius:t.getRadius()}:(t instanceof L.Marker||t instanceof L.CircleMarker)&&(this._uneditedLayerProps[e]={latlng:L.LatLngUtil.cloneLatLng(t.getLatLng())}))},_getTooltipText:function(){return{text:L.drawLocal.edit.handlers.edit.tooltip.text,subtext:L.drawLocal.edit.handlers.edit.tooltip.subtext}},_updateTooltip:function(){this._tooltip.updateContent(this._getTooltipText())},_revertLayer:function(t){var e=L.Util.stamp(t);t.edited=!1,this._uneditedLayerProps.hasOwnProperty(e)&&(t instanceof L.Polyline||t instanceof L.Polygon||t instanceof L.Rectangle?t.setLatLngs(this._uneditedLayerProps[e].latlngs):t instanceof L.Circle?(t.setLatLng(this._uneditedLayerProps[e].latlng),t.setRadius(this._uneditedLayerProps[e].radius)):(t instanceof L.Marker||t instanceof L.CircleMarker)&&t.setLatLng(this._uneditedLayerProps[e].latlng),t.fire("revert-edited",{layer:t}))},_enableLayerEdit:function(t){var e,i,o=t.layer||t.target||t;this._backupLayer(o),this.options.poly&&(i=L.Util.extend({},this.options.poly),o.options.poly=i),this.options.selectedPathOptions&&(e=L.Util.extend({},this.options.selectedPathOptions),e.maintainColor&&(e.color=o.options.color,e.fillColor=o.options.fillColor),o.options.original=L.extend({},o.options),o.options.editing=e),o instanceof L.Marker?(o.editing&&o.editing.enable(),o.dragging.enable(),o.on("dragend",this._onMarkerDragEnd).on("touchmove",this._onTouchMove,this).on("MSPointerMove",this._onTouchMove,this).on("touchend",this._onMarkerDragEnd,this).on("MSPointerUp",this._onMarkerDragEnd,this)):o.editing.enable()},_disableLayerEdit:function(t){var e=t.layer||t.target||t;e.edited=!1,e.editing&&e.editing.disable(),delete e.options.editing,delete e.options.original, +this._selectedPathOptions&&(e instanceof L.Marker?this._toggleMarkerHighlight(e):(e.setStyle(e.options.previousOptions),delete e.options.previousOptions)),e instanceof L.Marker?(e.dragging.disable(),e.off("dragend",this._onMarkerDragEnd,this).off("touchmove",this._onTouchMove,this).off("MSPointerMove",this._onTouchMove,this).off("touchend",this._onMarkerDragEnd,this).off("MSPointerUp",this._onMarkerDragEnd,this)):e.editing.disable()},_onMouseMove:function(t){this._tooltip.updatePosition(t.latlng)},_onMarkerDragEnd:function(t){var e=t.target;e.edited=!0,this._map.fire(L.Draw.Event.EDITMOVE,{layer:e})},_onTouchMove:function(t){var e=t.originalEvent.changedTouches[0],i=this._map.mouseEventToLayerPoint(e),o=this._map.layerPointToLatLng(i);t.target.setLatLng(o)},_hasAvailableLayers:function(){return 0!==this._featureGroup.getLayers().length}}),L.EditToolbar.Delete=L.Handler.extend({statics:{TYPE:"remove"},initialize:function(t,e){if(L.Handler.prototype.initialize.call(this,t),L.Util.setOptions(this,e),this._deletableLayers=this.options.featureGroup,!(this._deletableLayers instanceof L.FeatureGroup))throw new Error("options.featureGroup must be a L.FeatureGroup");this.type=L.EditToolbar.Delete.TYPE;var i=L.version.split(".");1===parseInt(i[0],10)&&parseInt(i[1],10)>=2?L.EditToolbar.Delete.include(L.Evented.prototype):L.EditToolbar.Delete.include(L.Mixin.Events)},enable:function(){!this._enabled&&this._hasAvailableLayers()&&(this.fire("enabled",{handler:this.type}),this._map.fire(L.Draw.Event.DELETESTART,{handler:this.type}),L.Handler.prototype.enable.call(this),this._deletableLayers.on("layeradd",this._enableLayerDelete,this).on("layerremove",this._disableLayerDelete,this))},disable:function(){this._enabled&&(this._deletableLayers.off("layeradd",this._enableLayerDelete,this).off("layerremove",this._disableLayerDelete,this),L.Handler.prototype.disable.call(this),this._map.fire(L.Draw.Event.DELETESTOP,{handler:this.type}),this.fire("disabled",{handler:this.type}))},addHooks:function(){var t=this._map;t&&(t.getContainer().focus(),this._deletableLayers.eachLayer(this._enableLayerDelete,this),this._deletedLayers=new L.LayerGroup,this._tooltip=new L.Draw.Tooltip(this._map),this._tooltip.updateContent({text:L.drawLocal.edit.handlers.remove.tooltip.text}),this._map.on("mousemove",this._onMouseMove,this))},removeHooks:function(){this._map&&(this._deletableLayers.eachLayer(this._disableLayerDelete,this),this._deletedLayers=null,this._tooltip.dispose(),this._tooltip=null,this._map.off("mousemove",this._onMouseMove,this))},revertLayers:function(){this._deletedLayers.eachLayer(function(t){this._deletableLayers.addLayer(t),t.fire("revert-deleted",{layer:t})},this)},save:function(){this._map.fire(L.Draw.Event.DELETED,{layers:this._deletedLayers})},removeAllLayers:function(){this._deletableLayers.eachLayer(function(t){this._removeLayer({layer:t})},this),this.save()},_enableLayerDelete:function(t){(t.layer||t.target||t).on("click",this._removeLayer,this)},_disableLayerDelete:function(t){var e=t.layer||t.target||t;e.off("click",this._removeLayer,this),this._deletedLayers.removeLayer(e)},_removeLayer:function(t){var e=t.layer||t.target||t;this._deletableLayers.removeLayer(e),this._deletedLayers.addLayer(e),e.fire("deleted")},_onMouseMove:function(t){this._tooltip.updatePosition(t.latlng)},_hasAvailableLayers:function(){return 0!==this._deletableLayers.getLayers().length}})}(window,document); \ No newline at end of file diff --git a/src/js/mapUtils.js b/src/js/mapUtils.js new file mode 100644 index 0000000..9acd825 --- /dev/null +++ b/src/js/mapUtils.js @@ -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; +} + diff --git a/src/js/sessionMgmt.js b/src/js/sessionMgmt.js new file mode 100644 index 0000000..f407575 --- /dev/null +++ b/src/js/sessionMgmt.js @@ -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); + } + }); +} \ No newline at end of file diff --git a/src/login.html b/src/login.html new file mode 100644 index 0000000..4702b53 --- /dev/null +++ b/src/login.html @@ -0,0 +1,53 @@ + + + + Login + + + + +

Login

+
+
+ + +
+
+ + +
+
+ +
+
+
+ + + + + diff --git a/src/logout.html b/src/logout.html new file mode 100644 index 0000000..4e860d1 --- /dev/null +++ b/src/logout.html @@ -0,0 +1,21 @@ + + + + Login + + + + + +

Ausgeloggt

+

Du hast dich erfolgreich ausgelogt.

+ + +