feat: Functional base done.

This commit is contained in:
KyMAN 2025-06-24 21:39:02 +02:00
parent cdb5857149
commit d6345aacdc
15 changed files with 982 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/Data
/Public/data
.sass-cache
*.[Ss]ecrets.*

View File

@ -0,0 +1,98 @@
"use strict";
import {Utils} from "../Utils/Utils.ecma.js";
import {EventModel} from "../Models/EventModel.ecma.js";
import {BaseView} from "../Views/BaseView.ecma.js";
import {MenuView} from "../Views/MenuView.ecma.js";
import {MapView} from "../Views/MapView.ecma.js";
/**
* @callback routes_maker_ready_callback
* @param {!RoutesMaker} routes_maker
* @returns {void}
*/
/**
* @class
* @constructor
* @param {!(string|HTMLElement)} [position = "body"]
* @param {?routes_maker_ready_callback} [callback = null]
* @returns {void}
* @access public
* @static
*/
export const RoutesMaker = (function(){
/**
* @constructs RoutesMaker
* @param {!(string|HTMLElement)} [position = "body"]
* @param {?routes_maker_ready_callback} [callback = null]
* @returns {void}
* @access private
* @static
*/
const RoutesMaker = function(position = "body", callback = null){
/** @type {RoutesMaker} */
const self = this;
/** @type {number} */
let latitude = 0,
/** @type {number} */
longitude = 0;
/** @type {HTMLElement|null} */
this.item_self = null;
/** @type {EventModel} */
this.on_ready = new EventModel();
/** @type {BaseView} */
this.base = new BaseView(self);
/** @type {MenuView} */
this.menu = new MenuView(self);
/** @type {MapView} */
this.map = new MapView(self);
const constructor = () => {
self.on_ready.add(callback);
navigator.geolocation.getCurrentPosition(location => {
longitude = location.coords.longitude;
latitude = location.coords.latitude;
build();
}, () => {
build();
});
};
/**
* @returns {void}
* @access private
*/
const build = () => {
Utils.preload(position, (position, asynchronous, ok) => {
if(ok){
try{
self.base.build(position);
self.map.build();
self.menu.build();
self.on_ready.autoexecute = true;
self.on_ready.execute(self);
}catch(exception){
console.error(exception);
};
};
});
};
/**
* @returns {[number, number]}
* @access public
*/
this.get_user_coordenates = () => [latitude, longitude];
constructor();
};
return RoutesMaker;
})();

View File

@ -0,0 +1,199 @@
"use strict";
import {EventModel} from "./EventModel.ecma.js";
/**
* @class
* @constructor
* @param {!L.Map} map
* @param {!number} x
* @param {!number} y
* @param {!number} i
* @param {!Array.<DotModel>} dots
* @returns {void}
* @access public
* @statix
*/
export const DotModel = (function(){
/**
* @constructs DotModel
* @param {!L.Map} map
* @param {!number} x
* @param {!number} y
* @param {!number} i
* @param {!Array.<DotModel>} dots
* @returns {void}
* @access private
* @statix
*/
const DotModel = function(map, x, y, i, dots){
/** @type {DotModel} */
const self = this;
/** @type {Array.<L.Map>} */
this.map = map;
/** @type {number} */
this.i = i;
/** @type {number} */
this.x = x;
/** @type {number} */
this.y = y;
/** @type {L.Marker} */
this.marker = L.marker([y, x], {draggable : true});
/** @type {L.Pulyline|null} */
this.line = null;
/** @type {Array.<DotModel>} */
this.dots = dots;
/** @type {number} */
this.distance = 0;
/** @type {EventModel} */
this.on_change = new EventModel();
/**
* @returns {void}
* @access private
*/
const constructor = () => {
self.marker.addTo(map);
dots.length && (self.line = L.polyline([dots[dots.length - 1].get_dot(), self.get_dot()]).addTo(self.map));
self.marker.on("click", event => {
self.remove();
});
self.marker.on("dragend", event => {
/** @type {L.LatLng} */
const coordenates = event.target.getLatLng();
self.x = coordenates.lng;
self.y = coordenates.lat;
self.remove_lines_linked();
self.i < self.dots.length - 1 && (self.dots[self.i + 1].line = L.polyline([
self.dots[self.i + 1].get_dot(),
self.get_dot()
])).addTo(self.map);
self.i && (self.dots[self.i].line = L.polyline([
self.get_dot(),
self.dots[self.i - 1].get_dot()
])).addTo(self.map);
self.recalculate_distance();
});
self.recalculate_distance();
};
/**
* @returns {Object.<string, number>}
* @access public
*/
this.get_leaflet_coordenates = () => ({
lat : self.y,
lng : self.x
});
/**
* @returns {Object.<string, number>}
* @access public
*/
this.get_coordenates = () => ({
latitude : self.y,
longitude : self.x
});
/**
* @returns {[string, number]}
* @access public
*/
this.get_dot = () => [self.y, self.x];
/**
* @returns {void}
* @access public
*/
this.remove_line = () => {
if(self.line){
self.map.removeLayer(self.line);
self.line = null;
};
};
/**
* @returns {void}
* @access public
*/
self.remove_lines_linked = () => {
/** @type {number} */
const l = self.dots.length;
[self.i, self.i + 1].forEach(i => {
i && i < l && self.dots[i].remove_line();
});
};
/**
* @returns {void}
* @access public
*/
this.remove = () => {
self.remove_lines_linked();
self.i && self.i < self.dots.length - 1 && (self.dots[self.i + 1].line = L.polyline([
self.dots[self.i + 1].get_dot(),
self.dots[self.i - 1].get_dot()
])).addTo(self.map);
self.dots.splice(self.i, 1);
console.log(["A", self.distance]);
self.dots[self.i + 1].recalculate_distance();
console.log(["B", self.distance]);
self.map.removeLayer(self.marker);
self.dots.slice(self.i).forEach(dot => {
dot.i --;
});
};
/**
* @returns {void}
* @access public
*/
this.recalculate_distance = () => {
/** @type {number} */
const origin = self.distance || 0;
if(self.i){
/** @type {DotModel} */
const previous = self.dots[self.i - 1],
/** @type {number} */
pi_radians = Math.PI / 180,
/** @type {number} */
angle = (
Math.sin((self.y - previous.y) * pi_radians / 2) ** 2 +
Math.cos(self.y * pi_radians) * Math.cos(previous.y * pi_radians) *
Math.sin((self.x - previous.x) * pi_radians / 2) ** 2
);
self.distance = DotModel.EARTH_RADIUS * 2 * Math.atan2(angle ** .5, (1 - angle) ** .5);
}else
self.distance = 0;
origin != self.distance && self.on_change.execute(self);
};
constructor();
};
/** @type {number} */
DotModel.EARTH_RADIUS = 6371000;
return DotModel;
})();

View File

@ -0,0 +1,87 @@
"use strict";
import {Check} from "../Utils/Check.ecma.js";
import {Utils} from "../Utils/Utils.ecma.js";
/**
* @callback event_model_callback
* @param {!number} i
* @param {...any} [parameters]
* @returns {Array.<any|null>}
*/
/**
* @class
* @constructor
* @returns {void}
* @access public
* @static
*/
export const EventModel = (function(){
/**
* @constructs EventModel
* @returns {void}
* @access private
* @static
*/
const EventModel = function(){
/** @type {EventModel} */
const self = this,
/** @type {Array.<event_model_callback>} */
events = [];
/** @type {boolean} */
this.autoexecute = false;
/**
* @param {...any} [parameters]
* @returns {Array.<any|null>}
* @access public
*/
this.execute = (...parameters) => events.map((event, i) => event ? event(i, ...parameters) : null);
/**
* @param {!event_model_callback} callback
* @returns {number|null}
* @access public
*/
this.add = callback => {
/** @type {number|null} */
let i = null;
if(Check.is_function(callback)){
if(self.autoexecute)
Utils.execute(callback);
else{
/** @type {number} */
const l = events.length;
for(i = 0; i < l; i ++)
if(!events[i])
break;
events[i] = callback;
};
};
return i;
};
/**
* @param {!number} i
* @returns {void}
* @access public
*/
this.remove = i => {
Check.is_index(i) && i < events.length && events[i] && (events[i] = null);
};
};
return EventModel;
})();

View File

@ -0,0 +1,65 @@
"use strict";
import {EventModel} from "./EventModel.ecma.js";
import {Utils} from "../Utils/Utils.ecma.js";
/**
* @class
* @constructor
* @param {!(string|HTMLElement)} selector
* @returns {void}
* @access public
* @static
*/
export const ScreenModel = (function(){
/**
* @constructs ScreenModel
* @param {!(string|HTMLElement)} selector
* @returns {void}
* @access private
* @static
*/
const ScreenModel = function(selector){
/** @type {ScreenModel} */
const self = this;
/** @type {number|null} */
let interval = null;
/** @type {number} */
this.x = 0;
/** @type {number} */
this.y = 0;
/** @type {HTMLElement|null} */
this.item = null;
/** @type {EventModel} */
this.on_change = new EventModel();
/**
* @returns {void}
* @access private
*/
const constructor = () => {
Utils.preload(selector, (item, asynchronous, ok) => {
if(ok){
self.item = item;
interval = setInterval(() => {
if(self.x != self.item.offsetWidth || self.y != self.item.offsetHeight){
self.x = self.item.offsetWidth;
self.y = self.item.offsetHeight;
this.on_change.execute(self.x, self.y, self.item);
};
}, 100);
};
});
};
constructor();
};
return ScreenModel;
})();

View File

@ -0,0 +1,85 @@
"use strict";
/**
* @class
* @constructor
* @returns {void}
* @access public
* @static
*/
export const Check = (function(){
/**
* @constructs Check
* @returns {void}
* @access private
* @static
*/
const Check = function(){};
/**
* @param {?any} item
* @returns {boolean}
* @access public
* @static
*/
Check.is_function = item => typeof item == "function";
/**
* @param {?any} item
* @returns {boolean}
* @access public
* @static
*/
Check.is_number = item => typeof item == "number";
/**
* @param {?any} item
* @returns {boolean}
* @access public
* @static
*/
Check.is_integer = item => Check.is_number(item) && item == item >> 0;
/**
* @param {?any} item
* @returns {boolean}
* @access public
* @static
*/
Check.is_index = item => Check.is_integer(item) && item >= 0;
/**
* @param {?any} item
* @returns {boolean}
* @access public
* @static
*/
Check.is_html_object = item => item && item.constructor == Object;
/**
* @param {?any} item
* @returns {boolean}
* @access public
* @static
*/
Check.is_string = item => typeof item == "string";
/**
* @param {?any} item
* @returns {boolean}
* @access public
* @static
*/
Check.is_null_or_undefined = item => item === undefined || item === null;
/**
* @param {?any} item
* @returns {boolean}
* @access public
* @static
*/
Check.is_array = item => item instanceof Array;
return Check;
})();

View File

@ -0,0 +1,145 @@
"use strict";
import {Check} from "./Check.ecma.js";
/**
* @callback utils_preload_callback
* @param {?HTMLElement} item
* @param {!boolean} asynchronous
* @param {!boolean} ok
* @returns {void}
*/
/**
* @callback utils_execute_callback
* @param {...any} [paramenters]
* @returns {any|null}
*/
/**
* @class
* @constructor
* @returns {void}
* @access public
* @static
*/
export const Utils = (function(){
/**
* @constructs Utils
* @returns {void}
* @access private
* @static
*/
const Utils = function(){};
/**
* @param {!(string|HTMLElement)} selector
* @param {!utils_preload_callback} callback
* @param {!(Document|HTMLElement)} [root = document]
* @param {!number} [timeout = 2000]
* @returns {void}
* @access public
* @static
*/
Utils.preload = (selector, callback, root = document, timeout = 2000) => {
if(!selector)
callback(null, false, false);
else if(Check.is_html_object(selector))
callback(selector, false, true);
else if(Check.is_string(selector) && (selector = selector.trim())){
/** @type {HTMLElement|null} */
let item = null;
try{
if(item = root.querySelector(selector)){
callback(item, false, true);
return;
};
}catch(exception){
callback(null, false, false);
return;
};
/** @type {number} */
const date = Date.now();
/** @type {number} */
let interval = setInterval(() => {
if(item = root.querySelector(selector)){
clearInterval(interval);
callback(item, true, true);
}else if(Date.now() - date > timeout){
clearInterval(interval);
callback(null, false, true);
};
}, 100);
}else
callback(null, false, false);
};
/**
* @param {!utils_execute_callback} callback
* @param {...any} [parameters]
* @returns {any|null}
* @access public
* @static
*/
Utils.execute = (callback, ...parameters) => Check.is_function(callback) ? callback(...parameters) : null;
/**
* @param {!(string|HTMLElement)} selector
* @param {!Object.<string, any|null>} attributes
* @param {!(HTMLElement|Document)} [position = document]
* @returns {void}
* @access public
* @static
*/
Utils.set_attributes = (selector, attributes, position = document) => {
(Check.is_string(selector) ? position.querySelectorAll(selector) : [selector]).forEach(item => {
for(const key in attributes){
if(/^on_?/i.test(key))
item.addEventListener(
key.substring(2).replace(/[^a-z]+/g, "").toLowerCase(),
event => Utils.execute(attributes[key], item, event)
);
else
item.setAttribute(key.replace(/_|([A-Z])/g, (_, character) => {
return character ? "-" + character.toLowerCase() : "-";
}), attributes[key]);
};
});
};
/**
* @param {!(string|HTMLElement|Document)} selector
* @param {!Array.<[string, Object.<string, any|null>|null, Array.<any>|null]} items
* @param {!(HTMLElement|Document)} [position = document]
* @returns {Array.<HTMLElement>}
* @access public
* @static
*/
Utils.set_html = (selector, items, position = document) => {
Check.is_string(selector) && (selector = position.querySelector(selector));
return items.map(([tag, attributes, childs]) => {
/** @type {HTMLElement} */
const item = selector.appendChild(document.createElement(tag));
attributes && Utils.set_attributes(item, attributes);
if(!Check.is_null_or_undefined(childs)){
if(Check.is_string(childs))
item.innerHTML = childs;
else if(Check.is_array(childs))
Utils.set_html(item, childs);
};
return item;
});
};
return Utils;
})();

View File

@ -0,0 +1,32 @@
"use strict";
import {Utils} from "../Utils/Utils.ecma.js";
export const BaseView = (function(){
const BaseView = function(routes_marker){
const self = this;
const constructor = () => {};
this.build = position => {
routes_marker.item_self ||
(routes_marker.item_self = Utils.set_html(position, [
["div", {
id : "routes-maker",
class : "routes-maker",
data_application : "RoutesMaker",
data_git : "https://git.k3y.pw/KyMAN/RoutesMaker",
data_url : "https://routesmaker.k3y.pw/",
data_author : "KyMAN"
}]
])[0]);
};
constructor();
};
return BaseView;
})();

View File

@ -0,0 +1,70 @@
"use strict";
import {Utils} from "../Utils/Utils.ecma.js";
import {EventModel} from "../Models/EventModel.ecma.js";
import {DotModel} from "../Models/DotModel.ecma.js";
import {ScreenModel} from "../Models/ScreenModel.ecma.js";
export const MapView = (function(){
const MapView = function(routes_marker){
const self = this;
let built = false,
/** @type {Array.<[number, number]>} */
dots = [];
/** @type {ScreenModel|null} */
this.screen = null;
/** @type {EventModel} */
this.on_dots_change = new EventModel();
/** @type {HTMLElement|null} */
this.box = null;
/** @type {L.Map|null} */
this.map = null;
const constructor = () => {};
this.build = () => {
if(!built && routes_marker.item_self){
built = true;
self.box = Utils.set_html(routes_marker.item_self, [
["div", {
id : "routes-maker-map",
class : "map",
data_show_markers : true,
data_show_routes : true
}]
])[0];
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom : 19,
attribution : '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(self.map = L.map("routes-maker-map").setView(routes_marker.get_user_coordenates(), 13));
(self.screen = new ScreenModel(self.box)).on_change.add((i, x, y, box) => {
self.map.invalidateSize(true);
});
self.map.on("click", event => {
const dot = new DotModel(self.map, event.latlng.lng, event.latlng.lat, dots.length, dots);
dots.push(dot);
recalculate_total_distance();
dot.on_change.add(() => {
recalculate_total_distance();
});
});
};
};
const recalculate_total_distance = () => {
routes_marker.item_self.querySelector(".map-menu [name=distance]").value = dots.reduce((distance, dot) => distance + dot.distance, 0) >> 0;
};
constructor();
};
return MapView;
})();

View File

@ -0,0 +1,99 @@
"use strict";
import {Utils} from "../Utils/Utils.ecma.js";
export const MenuView = (function(){
const MenuView = function(routes_marker){
const self = this;
let built = false;
const constructor = () => {};
this.build = () => {
if(!built && routes_marker.item_self){
built = true;
Utils.set_html(routes_marker.item_self, [
["form", {
class : "map-menu",
method : "GET",
action : "#",
on_submit : (form, event) => false
}, [
["fieldset", null, [
["legend", {data_i18n : "menu"}, "Menu"],
["nav", null, [
["ul", null, [
["li", {
data_i : 0,
data_field : "distance",
data_i18n : "distance",
data_i18n_without : true,
title : "Distance"
}, [
["label", {for : "distance"}, [
["span", {data_i18n : "distance"}, "Distance"],
["input", {
type : "text",
name : "distance",
readonly : true,
id : "distance"
}],
["span", {data_i18n : "meters_symbol"}, "m"]
]]
]],
["li", {
data_i : 1,
data_field : "show-markers",
data_i18n : "show_markers",
data_i18n_without : true,
title : "Show markers"
}, [
["label", {for : "show-markers"}, [
["input", {
type : "checkbox",
name : "show_markers",
id : "show-markers",
checked : true,
on_change : (item, event) => {
Utils.set_attributes(".map", {data_show_markers : item.checked}, routes_marker.item_self);
}
}],
["span", {data_i18n : "show_markers"}, "Show markers"]
]]
]],
["li", {
data_i : 2,
data_field : "show-routes",
data_i18n : "show_routes",
data_i18n_without : true,
title : "Show routes"
}, [
["label", {for : "show-routes"}, [
["input", {
type : "checkbox",
name : "show_routes",
id : "show-routes",
checked : true,
on_change : (item, event) => {
Utils.set_attributes(".map", {data_show_routes : item.checked}, routes_marker.item_self);
}
}],
["span", {data_i18n : "show_routes"}, "Show routes"]
]]
]]
]]
]]
]]
]]
]);
};
};
constructor();
};
return MenuView;
})();

40
Public/index.html Normal file
View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>RoutesMaker</title>
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link type="text/css;charset=utf-8" data-language="CSS3" rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" data-crossorigin="anonymous" charset="utf-8" />
<link type="text/css;charset=utf-8" data-language="SASS/CSS3" rel="stylesheet" href="/scss/RoutesMaker.css" data-scss="/scss/RoutesMaker.scss" data-css-map="/scss/RoutesMaker.css.map" data-crossorigin="anonymous" charset="utf-8" />
<style data-type="text/css;charset=utf-8" data-language="CSS3" charset="utf-8">
html,body{
height : 100%;
margin : 0em;
}
</style>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script type="module" data-type="text/javascript;charset=utf-8" data-language="ECMAScript 2015" charset="utf-8">
"use strict";
import {RoutesMaker} from "./ecma/Application/RoutesMaker.ecma.js";
try{
const routes_maker = new RoutesMaker("body", routes_maker => {
console.log("PASA");
});
}catch(exception){
console.error(exception);
};
</script>
</head>
<body></body>
</html>

View File

@ -0,0 +1,23 @@
.routes-maker {
position: relative; }
.routes-maker > .map {
position: absolute; }
.routes-maker, .routes-maker > .map {
top: 0em;
left: 0em;
width: 100%;
height: 100%;
overflow: hidden;
z-index: 10; }
.routes-maker > .map-menu {
position: absolute;
top: 5em;
left: 1em;
width: 20em;
z-index: 20; }
.routes-maker [data-show-markers=false] .leaflet-marker-pane, .routes-maker [data-show-markers=false] .leaflet-shadow-pane {
display: none; }
.routes-maker [data-show-routes=false] .leaflet-overlay-pane {
display: none; }
/*# sourceMappingURL=RoutesMaker.css.map */

View File

@ -0,0 +1,7 @@
{
"version": 3,
"mappings": "AAAA,aAAa;EAET,QAAQ,EAAG,QAAQ;EACnB,oBAAM;IAAC,QAAQ,EAAG,QAAQ;EAC1B,mCAAQ;IACJ,GAAG,EAAG,GAAG;IACT,IAAI,EAAG,GAAG;IACV,KAAK,EAAG,IAAI;IACZ,MAAM,EAAG,IAAI;IACb,QAAQ,EAAG,MAAM;IACjB,OAAO,EAAG,EAAE;EAGhB,yBAAW;IACP,QAAQ,EAAG,QAAQ;IACnB,GAAG,EAAG,GAAG;IACT,IAAI,EAAG,GAAG;IACV,KAAK,EAAG,IAAI;IACZ,OAAO,EAAG,EAAE;EAGU,0HAAyC;IAAC,OAAO,EAAG,IAAI;EAClF,4DAA8C;IAAC,OAAO,EAAG,IAAI",
"sources": ["RoutesMaker.scss"],
"names": [],
"file": "RoutesMaker.css"
}

View File

@ -0,0 +1,25 @@
.routes-maker{
position : relative;
&>.map{position : absolute;}
&,&>.map{
top : 0em;
left : 0em;
width : 100%;
height : 100%;
overflow : hidden;
z-index : 10;
}
&>.map-menu{
position : absolute;
top : 5em;
left : 1em;
width : 20em;
z-index : 20;
}
[data-show-markers=false]{.leaflet-marker-pane,.leaflet-shadow-pane{display : none;}}
[data-show-routes=false] .leaflet-overlay-pane{display : none;}
}

3
Tools/sass.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
directory=`dirname $(readlink -f "$0")`
sass $directory/../Public/scss/RoutesMaker.scss ../Public/scss/RoutesMaker.css;