fisrt init

This commit is contained in:
Red 2025-09-10 15:02:56 +05:00
commit bc1adf488a
43 changed files with 2758 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/data
/.git

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# ano-mr-site

94
db_get.php Normal file
View File

@ -0,0 +1,94 @@
<?php
require_once 'db_manager.php';
use function Catcher\recovery;
function gen_geojson($name, $features) {
return (object)[
"type" => "FeatureCollection",
"name" => $name,
"crs" => (object) [
"type" => "name",
"properties" => (object) [
"name" => "urn:ogc:def:crs:OGC:1.3:CRS84"
]
],
"features" => $features
];
}
function gen_feature($settlement) {
$settlement = (object) $settlement;
$settlement->info_exist = ($settlement->info_exist == 1)? true : false;
if($settlement->slider)
$settlement->slider = json_decode($settlement->slider);
if($settlement->images)
$settlement->images = json_decode($settlement->images);
$latitude = $settlement->latitude;
$longitude = $settlement->longitude;
unset($settlement->latitude);
unset($settlement->longitude);
return (object) [
"type" => "Feature",
"properties" => $settlement,
"geometry" => (object) [
"type" => "Point",
"coordinates" => [$longitude, $latitude]
]
];
}
function gen_json($obj) :string {
return json_encode($obj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
$body_raw = file_get_contents('php://input');
$body = json_decode($body_raw);
$mode = $body->mode;
$result = (object)[];
switch($mode) {
case 'geojsons':
$periods_geojson = (object) [];
$settlements = $db_manager->select_all();
foreach($settlements as $settlement) {
$period = ((object)$settlement)->period;
if(empty($periods_geojson->$period))
$periods_geojson->$period = [];
array_push($periods_geojson->$period, gen_feature($settlement));
};
foreach($periods_geojson as $name=>$features) {
$result->$name = gen_geojson($name, $features);
}
echo gen_json($result);
break;
case 'file':
$full_path = $body->path;
if (is_file($full_path)) {
// Определяем MIME-тип (например, для видео/mp4, изображений и т.д.)
$mime_type = mime_content_type($full_path);
// Устанавливаем заголовки
header('Content-Type: ' . $mime_type);
header('Content-Length: ' . filesize($full_path));
// Отдаём файл
readfile($full_path);
exit();
} else {
http_response_code(404);
echo json_encode(['error' => 'File not found']);
}
break;
default:
header('Content-Type: application/json');
echo gen_json((object)['msg' => "Не передан mode"]);
break;
}
?>

33
db_manager.php Normal file
View File

@ -0,0 +1,33 @@
<?php
require_once './lib/php/DataBaseManager/DBManager.php';
require_once './lib/php/DataBaseManager/Entitie.php';
require_once './lib/php/Catcher/Catcher.php';
use DataBaseManager\Entitie\Entitie;
use DataBaseManager\DBManager\DBManager;
use function Catcher\recovery;
$config = (object) [
'driver' => 'mysql',
'host' => 'localhost',
'port' => 3306,
'dbname' => 'anodb',
'username' => 'root',
'password' => 'root'
];
$entitie = new Entitie([
'name varchar(32) not null',
'type varchar(16) not null',
'period varchar(16) not null',
'longitude double not null',
'latitude double not null',
'info_exist tinyint(1) not null',
'slider json',
'images json',
'video text',
'background text'
], 'settlements');
$db_manager = new DBManager($entitie, $config);
?>

189
front/data-manager.js Normal file
View File

@ -0,0 +1,189 @@
/**
* @typedef {Object} MarkerData
* @property {Blob} background - фон
* @property {Array<Blob>} images - изображения
* @property {Array<Blob>} slider - слайды
*/
/** Путь к php-прослойке, работающая с бд и внутренним хранилищем */
const db_get_filepath = '../db_get.php'
/** Дефолтный срок годности кэша : 2 часа */
const CACHE_DEFAULT_TTL_MS = 2 * 60 * 60 * 1000;
/**
* Излечь данные из ответа
* @param {string} type - тип извлекаемых данных
* @param {Response} response - ответ
* @returns {Promise<Object|Blob>}
*/
const extract_file = recovery(async (type, response) => {
switch(type) {
case 'file':
return await response.blob()
case 'json':
return await response.json()
default:
throw new Error('Не определен type')
}
}, (...e) => {throw Error(`Ошибки при извлечении (extract_file): ${e.join(', ')}`)} )
/**
* @param {string} path - путь (либо локальный или https)
* @param {object} options - параметры fetch
* @param {int} timeout - секундомер
*/
const fetch_with_timeout = recovery(async (path, options = {}, timeout = 10 * 1000) => {
/** Штука для преждевременного завершения fetch */
const controller = new AbortController()
/** Как я понял, якорь для fetch */
const signal = controller.signal
/** Сработает завершение после истечения timeout */
const timeoutId = setTimeout(() => controller.abort(), timeout)
/** Запрос с привязкой якоря */
const response = await fetch(path, {...options, signal})
try {return response}
finally {clearTimeout(timeoutId)}
}, (e) => {
if(e.name === 'AbortError')
console.log('Запрос прерван по таймауту:', e)
else console.log(e)
}, (...e) => {throw Error(`Ошибки при запросе с таймером (fetch_with_timeout): ${e.join(', ')}`)} )
/**
* Вернуть данные из ответа, кэшируя перед этим
* @param {string} type - тип извлекаемых данных
* @param {string} path - путь (либо локальный или https)
* @param {Object} body_req - тело запроса
* @returns {Promise<Object|Blob>}
*/
const return_fetch = recovery(async (type, path, cache_key, body_req) => {
const cache = await caches.open('cache')
/** Запрос по пути (если есть тело, то локальное обращение к файлу) */
const response = !body_req ? await fetch_with_timeout(path) : await fetch_with_timeout(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json'},
body: JSON.stringify(body_req)
})
/** Клонирование, т.к blob/json необратимый процесс. */
const cloned = response.clone()
const blob = await cloned.blob()
const headers = new Headers(cloned.headers)
/** Добавление даты создания в заголовок 'date' кешируемого ответа. */
headers.set('date', new Date().toUTCString())
/** Процесс кэширования */
await cache.put(
cache_key,
new Response(blob, {
status: cloned.status,
statusText: cloned.statusText,
headers
})
)
/** Возвращаем оригинальный (первый) ответ */
return await extract_file(type, response)
}, (...e) => {throw Error(`Ошибки при запросе и кэшировании (return_fetch): ${e.join(', ')}`)} )
/**
* Получить данные из пути(локальный или https)
* @param {*} type - тип извлекаемых данных
* @param {*} path - путь (либо локальный или https)
* @param {*} body_req - тело запроса
* @returns {Promise<Object|Blob>}
*/
const get = recovery(async (type, path, body_req = null) => {
const cache = await caches.open('cache')
/** Ключ для добавления или получения кэш-данных. */
const cache_key = `${path}?type=${type}&file=${encodeURIComponent(JSON.stringify(body_req || {}))}`
/** Результат поиска ответа в кэше */
const cached_response = await cache.match(cache_key)
if (cached_response) {
/** Срок годности кэша (если задан) */
const expires_value = cached_response.headers.get('expires')
/** Дата создания кэша */
const date_value = cached_response.headers.get('date')
/** До какого числа годен */
const exp = expires_value && expires_value !== '-1'
? Date.parse(expires_value)
: Date.parse(date_value ?? '') + CACHE_DEFAULT_TTL_MS
/** Если exp больше, вернуть ответ из кэша, иначе удалить протухшие данные */
if (Date.now() < exp) {
return await extract_file(type, cached_response )
}
await cache.delete(cache_key)
}
/** Формирование нового кэша, т.к предыдущий протух или его нет */
return await return_fetch(type, path, cache_key, body_req)
}, (...e) => {throw Error(`Ошибки при получении (get): ${e.join(', ')}`)} )
/**
* Получить данные на диске (абсолютный/относительный путь)
* @param {string} filepath - путь к файлу
* @returns {Promise<Blob>} - без json
*/
const get_local = recovery(async (filepath) => {
return await get('file', db_get_filepath, {
mode: 'file',
path: filepath
})
}, (...e) => {throw Error(`Ошибки при локальном получении (get_local): ${e.join(', ')}`)} )
/**
* Получить данные без кэширования
* @param {string} type - тип извлекаемых данных
* @param {string} path - путь (либо локальный или https)
* @returns {Promise<Object|Blob>}
*/
const get_no_cached = recovery(async (type, path) => {
const file = await fetch_with_timeout(path)
return await extract_file(type, file)
}, (...e) => {throw Error(`Ошибки при получении без кэширования (get_no_cached): ${e.join(', ')}`)} )
/**
* Получить метериалы поселения для инфо. страницы
* @param {MarkerData} properties
* - нужные свойства (пути) из свойств feature.
* @returns {Promise<{ background: Blob, images: Blob[], slider: Blob[]}>}
*/
const get_for_info_page = recovery(async (properties) => {
/** Фон инфо. страницы */
const background = await get_local(properties.background)
/** Фотографи инфо. страницы */
const images = await Promise.all(
(properties.images || []).map(i_path => get_local(i_path))
)
/** Слайды инфо. страницы (если есть) */
const slider = Array.isArray(properties.slider)
? await Promise.all(properties.slider.map(s_path => get_local(s_path)))
: []
return {
background,
images,
slider,
}
}, (...e) => {throw Error(`Ошибки при получении материалов для страницы (get_for_info_page): ${e.join(', ')}`)} )
/**
* Получить иконки для стилизации страниц.
* @returns {Promise<Object|Blob>}
*/
const get_icons = recovery(async () => await get_no_cached ('json', './data/icons.json')
, (...e) => {throw Error(`Ошибки при получении данных для стилизации (get_icons): ${e.join(', ')}`)} )
/**
* Получить все geojson'ы из базы данных
* @returns {Promise<Object>}
*/
const get_all_geojsons = recovery(async () => await get('json', db_get_filepath, {'mode': 'geojsons'})
, (...e) => {throw Error(`Ошибки при получении geojson'ов из бд (get_all_geojsons): ${e.join(', ')}`)} )

Binary file not shown.

After

Width:  |  Height:  |  Size: 881 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

19
front/icons/icons.json Normal file
View File

@ -0,0 +1,19 @@
{
"markers": {
"Село": "./icons/markers/Selo.png",
"Деревня": "./icons/markers/Drevnya.png",
"Поселок": "./icons/markers/Poselok1.png",
"Юрта": "./icons/markers/Urta.png",
"Археообъект": "./icons/markers/Arch.png"
},
"others": {
"next": "./icons/style/next.png",
"previous": "./icons/style/previous.png",
"close": "./icons/style/close.svg",
"map": "./icons/func_btn/map_btn.jpg",
"slider": "./icons/func_btn/slider_btn.png",
"malaya_logo": "./icons/func_img/malaya_rodina.png",
"rmc_logo": "./icons/func_img/rmc_logo.png"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@ -0,0 +1,9 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" fill="#ffffff" stroke="#ffffff">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier">
<path fill="#ffffff" d="M195.2 195.2a64 64 0 0 1 90.496 0L512 421.504 738.304 195.2a64 64 0 0 1 90.496 90.496L602.496 512 828.8 738.304a64 64 0 0 1-90.496 90.496L512 602.496 285.696 828.8a64 64 0 0 1-90.496-90.496L421.504 512 195.2 285.696a64 64 0 0 1 0-90.496z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 747 B

BIN
front/icons/style/next.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 52 52" data-name="Layer 1" id="Layer_1" xmlns="http://www.w3.org/2000/svg">
<g data-name="Group 132" id="Group_132">
<path d="M14,52a2,2,0,0,1-1.41-3.41L35.17,26,12.59,3.41a2,2,0,0,1,0-2.82,2,2,0,0,1,2.82,0l24,24a2,2,0,0,1,0,2.82l-24,24A2,2,0,0,1,14,52Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 359 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" ?>
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 52 52" data-name="Layer 1" id="Layer_1" xmlns="http://www.w3.org/2000/svg">
<g data-name="Group 132" id="Group_132">
<path d="M38,52a2,2,0,0,1-1.41-.59l-24-24a2,2,0,0,1,0-2.82l24-24a2,2,0,0,1,2.82,0,2,2,0,0,1,0,2.82L16.83,26,39.41,48.59A2,2,0,0,1,38,52Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 359 B

103
front/index.html Normal file
View File

@ -0,0 +1,103 @@
<!--
MetaInfo
Author of the reissue: Diller(Кутман)
Date of change: 19.05.2025
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- Подключение OL -->
<link rel="stylesheet" href="../lib/ol/ol.css">
<script src="../lib/ol/ol.js"></script>
<!-- Подключение самописных модулей -->
<script src="../lib/js/catcher.js"></script>
<script src="../lib/js/page-manager.js"></script>
<script src="../lib/js/logger.js"></script>
<script src="../lib/js/single-layer-manager.js"></script>
<script src="../lib/js/ref-manager.js"></script>
<script src="../lib/js/popup-manager.js"></script>
<script src="../lib/js/slider.js"></script>
<!-- Подключение основных файлов -->
<link rel="stylesheet" href="./style.css">
<script src="./main.js"></script>
<script src="./data-manager.js"></script>
<!-- Файлы инициалиазции страниц -->
<script src="./page/main-page.js"></script>
<script src="./page/info-page.js"></script>
<script src="./page/slider-page.js"></script>
</head>
<body>
<!-- Область для страниц -->
<div id="page-space">
<!-- Главная страница -->
<div id="main-page">
<!-- Карта -->
<div id="map"></div>
<!-- UI-компоненты -->
<div id="main">
<div id="left-div">
<div class="common" id="logos"></div>
</div>
<div id="center-div">
<div class="common" id="title">
<p>Деньщиковская волость @Е-ГЕО</p>
</div>
</div>
<div id="right-div">
<div class="common" id="time-select">
</div>
<div class="common" id="legend"></div>
</div>
</div>
</div>
<!-- Страница информации -->
<div id="info-page">
<div id="info-page-images"></div>
<video id="info-page-video" controls="true"></video>
<!-- Модальное окно -->
<div id="myModal" class="modal">
<span class="close">&times;</span>
<img class="modal-content" id="modalImg">
</div>
</div>
<!-- Страница с слайдером -->
<div id="slider-page">
<div id="slider-container">
<div class="side" id="left">
<img id="previous">
</div>
<div id="center">
<div id="slider"></div>
</div>
<div class="side" id="right">
<img id="next">
</div>
</div>
<img id="to-info-page">
</div>
</div>
<!-- Служебные компоненты (не отрисовываются при загрузке) -->
<div id="popup">
<div id="popup-header">
<a id="popup-title"></a>
<img id="popup-close">
</div>
<div id="popup-content"></div>
</div>
</body>
</html>

34
front/main.js Normal file
View File

@ -0,0 +1,34 @@
/**
* @typedef {import ('./lib/js-lib/page-manager.js').PageManager} PageManager
*/
/**
* Основной элемент для управления страницами
* @type {PageManager}
*/
var page_manager
/** Основной метод */
const main = async () => {
page_manager = new PageManager('page-space',
{
element: 'main-page',
init: main_page_init,
},
{
element: 'info-page',
init: info_page_init,
always_mode: true
},
{
element: 'slider-page',
init: slider_page_init,
always_mode: true
}
)
page_manager.set_page('main-page', {page_manager})
}
window.onload = main

216
front/page/info-page.js Normal file
View File

@ -0,0 +1,216 @@
/**
* @typedef {import ('/lib/ref-manager.js').RefManager} RefManager
* @typedef {import('/lib/single-layer-manager.js').SingleLayerManager} SingleLayerManager
* @typedef {import ('/lib/popup-manager.js').PopupManager} PopupManager
* @typedef {import ('/lib/page-manager.js').PageManager} PageManager
*
* @typedef {Object} RefContent
* @property {string} background
* @property {Array<string>} images
* @property {Array<string>} slider
*
* @typedef {Object} RefData
* @property {RefContent} refs - ссылки
* @property {RefManager} refs_manager - менеджер ссылок (устаревший, теперь null)
*/
/** Вствка изображений в панель */
const add_img = (div, img_ref, onclick = () => {}) => {
/** Создание dom-изображения */
const img = document.createElement('img')
/** Инъекция ссылки */
img.src = img_ref
/** Инъекция поведения */
img.onclick = onclick
/** Вставка */
div.appendChild(img)
}
/**
* Функция инициализации инфо. страницы
* @param {PageManager} page_manager
* @function
*/
const info_page_init = async ({page_manager, icon_refs_manager = null, properties}) => {
/**
* Инициалиазция изображений и их помещение в колонку
* @function
* @param {Array<string>} images_refs - изображения маркера
* @param {HTMLElement} modal
* @param {HTMLElement} modal_img
*/
const images_init = async (images_refs, modal, modal_img) => {
/** Панель изображений из info-page */
const images = document.getElementById('info-page-images')
/** Вставка изображений в панель */
images_refs.forEach(
img_ref => add_img(
images,
img_ref,
() => {
modal.style.display = 'flex'
modal_img.src = img_ref
}
)
)
}
/**
* Инициалиазирует фон
* @function
* @param {string} background - ссылка на фон
*/
const background_init = (background) => {
const info_page = document.getElementById('info-page')
if (typeof background === 'string') {
// Заменяем \ на /
background = background.replace(/\\/g, '/')
// Убеждаемся, что путь начинается с /
if (!background.startsWith('/')) {
background = '/' + background
}
info_page.style.backgroundImage = `url(${background})`
} else {
info_page.style.backgroundImage = ''
}
}
/**
* Функция очистки после инита страницы
* @function
*/
const clear = () => {
/** Очистка картинок */
const images = document.getElementById('info-page-images')
/** Видео элемент */
const video = document.getElementById('info-page-video')
/** Очистка внутренностей элементов */
images.innerHTML = ''
if (video) {
video.pause()
video.removeAttribute('src')
video.load()
video.innerHTML = ''
video.style.display = 'none'
}
}
/**
* Инициализация модальной функциональности
* @function
* @returns {{modal: HTMLElement, modal_img: HTMLElement}}
*/
const modal_init = () => {
const close = document.querySelector('.close')
const modal = document.getElementById('myModal')
const modal_img = document.getElementById('modalImg')
/** Условия закрытия модального окна */
close.onclick = function () {
modal.style.display = 'none'
}
/** Тоже условие закрытия */
window.onclick = function (event) {
if (event.target == modal) {
modal.style.display = 'none'
}
}
return {modal, modal_img}
}
/**
* Инициализация видео
* @function
* @param {string} ref - прямая ссылка на видеофайл
*/
const video_init = async (ref) => {
const video = document.getElementById('info-page-video')
video.style.display = 'block'
// Очистка предыдущих источников
video.innerHTML = ''
const source = document.createElement('source')
source.src = ref
source.type = 'video/mp4'
video.appendChild(source)
video.load()
}
/**
* Создание функциональных кнопок (назад, к слайдеру)
* @function
* @param {string} to_map - ссылка на иконку кнопки к карте (назад)
* @param {string} to_slider - ссылка на иконку кнопки слайдер
* @param {PageManager} page_manager - менеджер страниц
*/
const func_btn_init = (to_map, to_slider, page_manager, icon_refs_manager, properties) => {
const images = document.getElementById('info-page-images')
if (to_slider && typeof to_slider === 'string') {
add_img(images, to_slider, () => {
clear()
page_manager.set_page('slider-page', {
page_manager,
icon_refs_manager,
properties
})
})
}
if (to_map && typeof to_map === 'string') {
add_img(images, to_map, () => {
clear()
page_manager.set_page('main-page', {page_manager})
})
}
}
/** Защищенный вызов */
recovery(
async () => {
// /** Получение информации маркера (ссылки напрямую) */
// const data = await get_for_info_page(properties)
// /** Инициализация ref-менеджер и создание ссылок */
// const {refs} = await refs_data_init(data)
/** Элементы modal */
const {modal, modal_img} = modal_init()
background_init(properties.background)
images_init(properties.images, modal, modal_img)
if (properties.video && typeof properties.video === 'string') {
video_init(properties.video)
}
func_btn_init(
icon_refs_manager.get("map"),
properties.slider.length <= 0 ? null : icon_refs_manager.get("slider"),
page_manager,
icon_refs_manager,
properties
)
},
(...e) => {
console.log("Ошибка при инициалиазции main-page")
e.forEach(er => {throw er})
}
) ()
}

344
front/page/main-page.js Normal file
View File

@ -0,0 +1,344 @@
/**
* @typedef {import ('/lib/js/ref-manager.js').RefManager} RefManager
* @typedef {import('/lib/js/single-layer-manager.js').SingleLayerManager} SingleLayerManager
* @typedef {import ('/lib/js/popup-manager.js').PopupManager} PopupManager
* @typedef {import ('/lib/js/page-manager.js').PageManager} PageManager
*/
/**
* Генерация функции для получения стилей маркера
* @function
* @param {Object<string, string>} marker_refs - объект с ссылками
* @returns {((feature) => ol.style.Style)}
*/
const gen_get_style = recovery(
marker_refs => feature => recovery(
() => {
/** Проверка feature */
if (!feature || typeof feature.getProperties !== 'function') {
throw new Error("Некорректный feature");
}
/** Свойства feature */
const properties = feature.getProperties()
/** Ссылка к иконке */
const url = marker_refs[properties.type.toLowerCase()]
?? marker_refs['археообъект'.toLowerCase()]
/** Генерация стилей */
return new ol.style.Style({
image: new ol.style.Icon({
anchor: [0.5, 1],
scale: 0.08,
src: url
})
})
},
(...e) => {
console.log("Ошибка при получении стилей (gen_get_style->get_style)");
e.forEach(er => {throw er});
}
)(),
(...e) => {
console.log("Ошибка генерации функции стилей (gen_get_style)");
e.forEach(er => {throw er});
}
)
/**
* Функция инициализирует карту
* @function
* @param {{coordinates: [number, number], target?: string}} arg
* @returns {{map: ol.Map, layer: ol.layer.Vector}}
*/
const create_map_and_layer = ({coordinates, target = "map"}, style_f) => {
/** Одиночный слой */
let layer = new ol.layer.VectorImage({
source: new ol.source.Vector({}),
style: (feature) => style_f(feature)
})
/** Создание карты и её привязка к dom-элементу */
const map = new ol.Map({
view: new ol.View({
center: ol.proj.fromLonLat(coordinates),
zoom: 10,
minZoom: 8
}),
layers: [
new ol.layer.Tile({
source: new ol.source.OSM(),
}),
layer
],
target: target
})
return {map, layer}
}
/**
* Активация стилей кнопки
* @function
*/
const active = (btn) => btn.classList.replace('not-selected', 'selected')
/**
* Деактивация стилей кнопки
* @function
*/
const deactive = (btn) => btn.classList.replace('selected', 'not-selected')
/**
* Функция инициализации главой страницы
* @function
* @param {PageManager} page_manager
* @param {RefManager} [previous_refs_manager] - менеджер ссылок предыдущей страницы
*/
const main_page_init = async ({page_manager}) => {
/**
* Cоздание ссылок из загруженных фотографий
* @function
* @returns {RefManager}
*/
const icon_refs_manager_init = async () => {
/** Данные (иконки) из внешнего json */
const icons_data = (await get('json', './icons/icons.json'))
const now_icons_data = {...icons_data.markers, ...icons_data.others}
/** Менеджер иконок, заполненный ссылками */
let icon_ref_manager = new RefManager()
for (const [name, path] of Object.entries(now_icons_data))
icon_ref_manager.save({
key: name.toLowerCase(),
ref: URL.createObjectURL(await get('file', path))
})
return icon_ref_manager
}
/**
* Создание и ининциализация карты
* @function
* @param {RefManager} icon_ref_manager - менеджер ссылок
* @returns {{map: ol.Map, layer: ol.layer.Vector}}
*/
const map_init = (icon_refs_manager) =>
create_map_and_layer(
{
coordinates: [70.008408, 60.001500],
target: "map"
},
gen_get_style(icon_refs_manager.get_all())
)
/**
* Создание и заполнение элемента с кнопками периодов
* @function
* @param {Object} geojsons - объект, содержащий все geojson'ы
*/
const periods_init = (geojsons, single_layer_manager) => {
/** DOM-элемент, куда кнопки периодов загружаются */
const time_select = document.getElementById('time-select')
const periods = Object.keys(geojsons)
.sort((a, b) => parseInt(a) - parseInt(b))
/** Массив кнопок с период-кнопками */
const buttons_periods = periods.map(period => {
/** Период-кнопка */
const button = document.createElement('button')
button.dataset.period = period;
button.textContent = period;
/** Добавление стилей: неактивная кнопка */
button.classList.add('not-selected')
/** Добавление в панель */
time_select.appendChild(button)
return button
})
/** Добавление слушателей (Загрузка geojson + сохранение состояния) */
buttons_periods.forEach(bp => {
bp.addEventListener('click', async () => {
/** Изменение слоя на карте */
single_layer_manager.set(geojsons[bp.dataset.period])
/** Сохранение состояния */
localStorage.setItem('selected', bp.dataset.period);
/** Выключение всех остальных кнопок */
buttons_periods.forEach(deactive)
/** Включение текущей кнопки */
active(bp)
})
})
/** Загрузка последнего сохраненного статуса-периода (Имитация выбора) */
const selected = localStorage.getItem('selected')
const button_select = buttons_periods.find(bp => bp.dataset.period === selected);
if (button_select) {
button_select.click()
} else
buttons_periods[0].click()
}
/**
* Создание и заполнение легенды (маркер - название)
* @function
* @param {RefManager} icon_refs_manager - менеджер ссылок
*/
const legend_init = (icon_refs_manager) => {
/** DOM-элемент легенды */
const legend = document.getElementById('legend')
for(const [name, ref] of Object.entries(icon_refs_manager.get_all())) {
/** Row в легенде */
const div = document.createElement('div')
/** Иконка - маркер */
const marker = document.createElement('img')
/** Текст - название */
const p = document.createElement('p');
/** Помещение данных в row */
marker.src = ref
p.innerHTML = name
/** Добавление в контейенер легенды */
div.appendChild(marker)
div.appendChild(p)
legend.appendChild(div)
}
}
/**
* Создание логотипов с переходом на другие страницы
* @function
* @param {RefManager} icon_refs_manager - менеджер ссылок
*/
const logos_init = (icon_refs_manager) => {
/** DOM-элемент для логотипов */
const logos = document.getElementById('logos')
/**
* Линковка ссылок на изображения и onclick'ов
* @param {string} img_src
* @param {(() => {})} onclick
* @returns
*/
const logo_init = (img_src, onclick) => {
/** Создание dom-элемента */
const logo = document.createElement('img')
/** Инъекция аргументов */
logo.src = img_src
logo.onclick = onclick
return logo
}
/** Создание и добавление логотипов */
[
logo_init(
icon_refs_manager.get('malaya_logo'),
() => window.location.href = 'https://vk.com/anomalaya_rodina'
),
logo_init(
icon_refs_manager.get('rmc_logo'),
() => window.location.href = 'https://vk.com/rcod_hmao'
)
].forEach(logo => logos.appendChild(logo));
}
/**
* Инициализация popup-manager
* @function
* @param {ol.Map} map - ol-карта
* @param {PageManager} page_manager - менеджер страниц
* @param {RefManager} refs_manager - менеджер ссылок
* @param {string} close_icon - икнонка крестика
* @returns {PopupManager}
*/
const popup_manager_init = (map, page_manager, refs_manager, close_icon) => {
/** Смещение popup */
const offset = [0, 10]
/** Контейнер popup */
const popup = document.getElementById('popup')
/** Закрывашка, привязка ссылки иконки*/
const popup_close = document.getElementById('popup-close')
popup_close.src = close_icon
/** Заголовок всплывающего окна */
const popup_title = document.getElementById('popup-title')
/** Контент всплывающего окна */
const popup_content = document.getElementById('popup-content')
/**
* Поведение карты при нажатии на карту
* @param {ol.Feature} feature - объект маркера
*/
const map_on = feature => {
/** Формирование контента */
popup_title.innerHTML = feature.getProperties().name
popup_content.innerHTML = ''
/** Формирование кнопки 'подробнее' и его привязка */
if (feature.getProperties().info_exist) {
/** Создание ссылки 'подробнее' */
const content_description = document.createElement('a')
content_description.innerHTML = 'подробнее'
content_description.href = '#'
/** Привязка */
popup_content.appendChild(content_description)
/** Переход к info-page */
content_description.onclick = () =>
page_manager.set_page('info-page', {
page_manager: page_manager,
icon_refs_manager: refs_manager,
properties: feature.getProperties()
})
}
}
return new PopupManager({
map: map,
popup_div: popup,
popup_close: popup_close,
offset: offset,
map_on: map_on
})
}
/** Защищенный вызов */
await recovery(
async () => {
/** Получение всех иконок */
const icon_ref_manager = await icon_refs_manager_init()
/** Инициализация */
const {map, layer} = map_init(icon_ref_manager)
const single_layer_manager = new SingleLayerManager(layer)
/** Получение всех geojson'ов */
const geojsons = await get_all_geojsons()
periods_init(geojsons, single_layer_manager)
legend_init(icon_ref_manager)
logos_init(icon_ref_manager)
popup_manager_init(map, page_manager, icon_ref_manager, icon_ref_manager.get('close'))
},
(...e) => {
console.log("Ошибка при инициалиазции main-page");
e.forEach(er => {throw er});
}
)()
}

68
front/page/slider-page.js Normal file
View File

@ -0,0 +1,68 @@
/**
* @typedef {import('../lib/js-lib/slider.js').Slider} Slider
* @typedef {import('../lib/js-lib/page-manager.js').PageManager} PageManager
* @typedef {import('../lib/js-lib/ref-manager.js').RefManager} RefManager
*
* @typedef {Object} SliderPageArg
* @property {RefManager} slider_urls_manager - менеджер для слайдера
* @property {RefManager} icon_refs_manager - менеджер иконок
* @property {PageManager} page_manager - менеджер страниц
* @property {Object} properties - свойства маркера
*/
/**
* Инициализация slider-page
* @function
* @param {SliderPageArg} arg
*/
const slider_page_init = ({page_manager, icon_refs_manager, properties}) => {
/**
* Инициализация кнопки возврата на info_page
* @function
*/
const to_info_page_init = (slider) => {
const to_info_page = document.getElementById('to-info-page');
// Иконка кнопки
to_info_page.src = icon_refs_manager.get("map");
to_info_page.onclick = () => {
// Очистка
slider.clear?.();
page_manager.set_page('info-page', {
page_manager,
icon_refs_manager,
properties
});
};
};
/** Безопасный вызов */
recovery(
() => {
// Получение изображений из менеджера
const images = properties.slider
// Создание слайдера
const slider = new Slider({
slider: {
main: document.getElementById('slider'),
next: document.getElementById('next'),
previous: document.getElementById('previous')
},
next_ref: icon_refs_manager.get('next'),
previous_ref: icon_refs_manager.get('previous'),
images
});
// Кнопка назад к info_page
to_info_page_init(slider);
},
(...e) => {
console.log("Ошибка при инициализации (slider-page)");
e.forEach(er => { throw er });
}
)();
};

316
front/style.css Normal file
View File

@ -0,0 +1,316 @@
/** MetaInfo
* Author of the reissue: Diller(Кутман)
* Date of change: 19.05.2025
*/
body {
margin: 0;
padding: 0;
background-color: #222;
overflow: hidden;
}
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
}
.modal-content {
margin: auto;
display: block;
width: 100%;
max-width: 1000px;
}
.close {
position: absolute;
top: 15px;
right: 35px;
color: #fff;
font-size: 40px;
font-weight: bold;
cursor: pointer;
}
#page-space {
height: 100vh;
width: 100vw;
}
#main-page {
width: 100%;
height: 100%;
}
#info-page {
display: flex;
flex-direction: column;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
width: 100%;
height: 100%;
}
#info-page-images {
display: flex;
flex-direction: column;
width: 150px;
height: 100%;
}
#info-page-images img {
object-fit: cover;
background-position: center;
margin: 10px 10px 0px 10px;
max-height: 100px;
}
#info-page-video {
display: none;
position: fixed;
height: 250px;
bottom: 10px;
left: 10px;
}
#map {
position: fixed;
width: 100vw;
height: 100vh;
z-index: 1;
}
#main {
display: flex;
flex-direction: row;
width: 100vw;
height: 100vh;
z-index: 0;
}
#left-div {
display: flex;
width: 20vw;
align-items: center;
}
#logos {
display: flex;
flex-direction: column;
justify-content: center;
z-index: 2;
width: 75px;
height: 160px;
}
#logos img {
margin: 15px;
}
#center-div {
display: flex;
flex-grow: 1;
justify-content: center;
align-items: start;
}
#title {
display: flex;
z-index: 2;
width: 600px;
height: 80px;
justify-content: center;
align-items: center;
}
#title p {
margin: 0;
/* color: aliceblue */
font-size: 38px;
}
#right-div {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: end;
width: 20vw;
}
#time-select {
display: flex;
flex-direction: column;
overflow: hidden;
border-radius: 10px;
z-index: 2;
background-color: rgba(99, 99, 99, 0);
width: 150px;
height: 250px;
}
#time-select button {
background-color: rgba(99, 99, 99, 0.8);
border: 0;
height: 10%;
}
#time-select button:hover {
background-color: #666;
}
#time-select button.selected {
background-color: #666;
}
#legend {
display: flex;
flex-direction: column;
z-index: 2;
margin-bottom: 20px;
width: 170px;
height: 230px;
}
#legend div {
display: flex;
flex-direction: row;
}
#legend div p {
display: flex;
justify-content: center;
margin: 13px;
width: 80%;
}
#legend div img {
height: 26px;
margin: 10px;
}
.common {
background-color: rgba(99, 99, 99, 0.8);
border-radius: 10px;
opacity: 0.9;
margin: 10px;
}
/* Main End */
#popup {
flex-direction: column;
background-color: rgba(6, 6, 6, 0.8);
border-radius: 10px;
z-index: 2;
width: 220px;
height: 100px;
}
#popup-header {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
#popup-title {
display: flex;
margin: 10px 0px 0px 10px;
justify-content: center;
color: aliceblue;
font-size: x-large;
}
#popup-close {
position: relative;
right: 5px;
top: 5px;
right: 10px;
width: 20px;
}
#popup-content {
margin: 10px;
color: aliceblue;
font-size: medium;
}
/* Slider Start */
#slider-container {
display: flex;
flex-direction: row;
height: 100vh;
width: 100vw;
}
.side {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 10%;
min-width: 150px;
z-index: 10;
cursor: pointer;
}
.side img {
height: 65px;
}
#center {
display: flex;
flex-grow: 1;
overflow: hidden;
position: relative;
}
#slider {
display: flex;
flex-direction: row;
height: 100%;
transition: transform 0.5s ease;
}
.slide {
height: 100%;
width: 80vw;
flex-shrink: 0;
object-fit: contain;
}
#to-info-page {
position: absolute;
left: 10px;
bottom: 10px;
height: 100px;
z-index: 100;
}
/* Slider End */

159
init_db.php Normal file
View File

@ -0,0 +1,159 @@
<?php
require_once 'db_manager.php';
use function Catcher\recovery;
$period_view = (object) [
'rArcheology' => '16 век и ранее',
'r1675' => '17 век',
'r1740' => '18 век',
'r1781' => '19 век',
'r1858' => '1901-1920',
'r1900' => '1921-1940',
'r1926' => '1941-1960',
'r1936' => '1961-1980',
'r1946' => '1981-2000',
'r200-now' => '2000 и н.в.'
];
$get_fs = null;
$get_fs = recovery(function (string $path) use (&$get_fs) {
$result = (object) ['files' => (object)[]];
$dirs = array_filter(scandir($path), function ($dir) {
return $dir !== '.' && $dir !== '..';
});
foreach($dirs as $dir) {
$new_path = $path . DIRECTORY_SEPARATOR . $dir;
if(is_dir($new_path))
$result->$dir = $get_fs($new_path);
else if(is_file($new_path)) {
$filename = pathinfo($new_path, PATHINFO_FILENAME);
$result->files->$filename = $new_path;
}
}
return $result;
}, function($e) {
throw $e;
});
$main = recovery(function (object $period_view) use (&$get_fs,&$db_manager)
{
$db_manager->delete_table();
$db_manager->create_table();
$fs = $get_fs('.' . DIRECTORY_SEPARATOR . 'data');
$removeLeadingDot = function(?string $path): ?string {
if ($path === null) return null;
return preg_replace('#^\.(?=[/\\\\])#', '', $path);
};
$removeLeadingDotFromJsonArray = function(?string $json): ?string {
if ($json === null) return null;
$arr = json_decode($json);
if (!is_array($arr)) return $json;
$arr = array_map(fn($p) => preg_replace('#^\.(?=.*)#', '', $p), $arr);
return json_encode($arr, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
};
$settlements_data = [];
$material_path = $fs->material;
foreach ($fs->geojson->files as $geojson_path) {
if (!file_exists($geojson_path)) {
echo 'Невалидный geojson-path : ' . $geojson_path;
continue;
}
$geojson = json_decode(file_get_contents($geojson_path));
$features = $geojson->features;
$basename = pathinfo($geojson_path, PATHINFO_FILENAME);
$period = $period_view->$basename;
foreach($features as $feature) {
$properties = $feature->properties;
$coordinates = $feature->geometry->coordinates;
$en = $properties->en;
$info_exist = 0;
$background = null;
$images = null;
$slider = null;
$video = null;
if(!empty($en) && isset($material_path->$en)) {
$material = $material_path->$en;
$tmp = array_values((array)$material->background->files);
$background = end($tmp);
if (!empty($material->image) && isset($material->image)) {
$tmp = array_values((array)$material->image->files);
$images = json_encode($tmp, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: null;
}
if (!empty($material->slider) && isset($material->slider)) {
$tmp = array_values((array)$material->slider->files);
$slider = json_encode($tmp, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: null;
}
if (!empty($material->video) && isset($material->video)) {
$tmp = array_values((array)$material->video->files);
$video = end($tmp);
}
$info_exist = 1;
}
switch (mb_strtolower($properties->Tupe)) {
case 'археология':
$type = 'археообъект';
break;
case 'юрты':
$type = 'юрта';
break;
default:
$type = $properties->Tupe;
}
// Убираем точку в путях перед записью в массив
$background = $removeLeadingDot($background);
$images = $removeLeadingDotFromJsonArray($images);
$slider = $removeLeadingDotFromJsonArray($slider);
$video = $removeLeadingDot($video);
array_push($settlements_data, [
'name' => $properties->Name,
'type' => $type,
'period' => $period,
'longitude' => $coordinates[0],
'latitude' => $coordinates[1],
'info_exist' => $info_exist,
'slider' => $slider,
'images' => $images,
'video' => $video,
'background' => $background
]);
}
}
$settlements_arg = array_map(
fn($settlements) => array_values((array) $settlements),
$settlements_data
);
$db_manager->create_all($settlements_arg);
}, function($e) {
throw $e;
});
$main($period_view);
?>

36
lib/js/catcher.js Normal file
View File

@ -0,0 +1,36 @@
/** тип async function */
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor
/**
* Безопасный запуск востановителя
* @param {(...errs: Error[]) => void} restorer
*/
const start_restorer = (restorer) => (...errs) => {
try {
return restorer(...errs)
} catch (e) {
console.error(e)
throw e
}
}
/**
* Функция отлова исключений
* @param {(...args: any[]) => any} func
* @param {(...errs: Error[]) => void} restorer
* @returns {(...args: any[]) => any}
*/
const recovery = (func, restorer = (...args) => {throw new Error(...args)}) => {
let resFunc
if(func instanceof AsyncFunction)
resFunc = async (...args) => {
try {return await func(...args)}
catch (e) {return start_restorer(restorer)(e)}
}
else
resFunc = (...args) => {
try {return func(...args)}
catch (e) {return start_restorer(restorer)(e)}
}
return resFunc
}

49
lib/js/logger.js Normal file
View File

@ -0,0 +1,49 @@
/**
* @returns {string}
*/
const getCurrentTime = () => {
const date = new Date()
return '' +
String(date.getFullYear()) + '.' +
String(date.getMonth()+1).padStart(2, '0') + '.' +
String(date.getDate()).padStart(2, '0') + ' ' +
String(date.getHours() + 5).padStart(2, '0') + ':' +
String(date.getMinutes()).padStart(2, '0') + ':' +
String(date.getSeconds()).padStart(2, '0')
}
/**
* @typedef {Object} Logger
* @property {(msg: string) => void} print
* @property {(wmsg: string) => void} wprint
* @property {(emsg: string) => void} eprint
* @property {(msg: string) => void} save
*/
/** @type {Logger} */
const logger = {
saved: [],
errors: [],
warnings: [],
logs: [],
/** @param {string} msg */
print: (msg) => {
const msg_format = `${getCurrentTime()} : Log : ${msg}`
logger.logs.push(msg_format);
console.log(msg_format)
},
/** @param {string} wmsg */
wprint: (wmsg) => {
const msg_format = `${getCurrentTime()} : Warning : ${wmsg}`
logger.warnings.push(msg_format);
console.log(msg_format)
},
/** @param {string} emsg */
eprint: (emsg) => {
const msg_format = `${getCurrentTime()} : Error : ${emsg}`
logger.errors.push(msg_format);
console.log(msg_format)
},
/** @param {string} msg */
save: (msg) => logger.saved.push(`${getCurrentTime()} : Data : ${msg}`),
}

67
lib/js/page-manager.js Normal file
View File

@ -0,0 +1,67 @@
/**
* @typedef {Object} Page
* @property {HTMLElement} element
* @property {(...args: any[]) => void} init
* @property {boolean} always_mode
* @property {boolean} [first_init]
*/
/**
* @param {string | HTMLElement} element
* @returns {HTMLElement}
*/
const get_element = recovery((element) => {
if(element instanceof HTMLElement)
return element
else if(typeof element === 'string')
return document.getElementById(element)
else
throw new Error(`element is not correct: ${element}`)
})
/**
* @class PageManager
* @property {Object<string, Page>} pages
* @property {HTMLElement} page_space
*/
class PageManager {
/**
* @param {string | HTMLElement} page_space_id
* @param {...Page} args
*/
constructor(page_space_id, ...args) {
recovery((page_space_id_f, ...args_f) => {
this.pages = {}
this.page_space = get_element(page_space_id_f)
let first_id
args_f.forEach((arg, index) => {
const {element, init = () => {}, always_mode = false} = arg
const el = get_element(element)
if(el instanceof HTMLElement){
const id = el.id?.trim() || `_page_${index}`
this.pages[id] = {element: el, init, always_mode}
if(first_id == null) first_id = id
} else
logger.wprint(`${element} is not html-element or id`)
})
}, logger.eprint) (page_space_id, ...args)
}
/**
* @param {string} page_id
* @param {...*} args
*/
set_page = recovery((page_id, ...args) => {
const page = this.pages[page_id]
this.page_space.innerHTML = ''
this.page_space.appendChild(page.element)
if(!page.first_init || page.always_mode) {
page.init(...args)
if(!page.first_init) page.first_init = true
}
}, logger.eprint)
}

127
lib/js/popup-manager.js Normal file
View File

@ -0,0 +1,127 @@
/**
* @typedef {Object} PopupManagerArg
* @property {ol.Map} map - ol-карта
* @property {HTMLElement} popup_div - контейнер всплывающего окна
* @property {HTMLElement} [popup_close] - закрывашка
* @property {(feature: ol.Feature) => void} map_on - поведение при нажатии на map
* @property {[number, number]} offset - смещение popup
*/
/**
* Управление и создание ol-popup
*
* @class PopupManager
* @property {ol.Map} map - ol-карта
* @property {HTMLElement} popup_div - контейнер всплывающего окна
* @property {HTMLElement} [popup_close] - закрывашка
* @property {(feature: ol.Feature) => void} map_on - поведение при нажатии на map
*/
class PopupManager {
/**
* @param {PopupManagerArg} args
*/
constructor ({map, popup_div, popup_close, map_on = (feature) => {}, offset = [0, 0]}) {
/** Безопасный вызов */
recovery(
/**
* @function
* @param {PopupManager} self
*/
self => {
/** Инъекция карты */
if(map && map instanceof ol.Map) self.map = map
else throw new Error("Аргумент 'map' невалиден")
/** Инъекция поведения при нажатии на map */
this.set_map_on(map_on)
/** Инъекция popup-элемента */
if(popup_div && popup_div instanceof HTMLElement)
self.popup_div = popup_div
else throw new Error("Аргумент 'popup_div' невалиден")
/** Инициализация popup-элемента */
self.overlay = new ol.Overlay({
element: self.popup_div,
autoPan: true,
offset: offset
})
/** Регристрация overlay */
self.map.addOverlay(self.overlay)
/** Добавление поведения при нажатии на карту */
self.map.on('click', evt => {
/** Получение данных маркера */
const feature = self.map.forEachFeatureAtPixel(
evt.pixel,
(feature, _) => feature,
{ hitTolerance: 8 }
)
if(feature) {
/** Вызов переданной функции */
self.map_on(feature)
/** Прилинковка popup */
self.set_popup(feature.getGeometry().getCoordinates())
} else {
/** Олинковка popup */
self.set_popup()
}
})
/** При нажатии не на маркеры убрать popup */
document.addEventListener('click', event => {
if (!event.target.closest('.ol-viewport') && !event.target.closest('.popup')) {
self.set_popup()
}
})
/** Инъекция и инициалиазция закрывашки (если есть) */
if(popup_close) {
/** Инъекция закрывашки */
if (popup_close instanceof HTMLElement) self.popup_close = popup_close
else throw new Error("Аргумент 'popup_close' невалиден")
/** Инициаилазиця закрывашки */
self.popup_close.onclick = function () {
/** Отлинковка от координат */
self.set_popup()
/** Скрытие */
popup_close.blur()
return false
}
}
},
(...e) => {
console.log("Ошибка при инициалиазции PopupManager");
e.forEach(er => {throw er});
}
) (this)
}
/**
* @function
* @param {[number, number] | undefined} [coord] - координаты привязки
*/
set_popup = recovery(
(coord = undefined) => this.overlay.setPosition(coord),
(...e) => {
console.log("Ошибка скрытии popup (PopupManager.set_popup)");
e.forEach(er => {throw er});
}
)
/**
* Изменение поведения при нажатии на map
* @function
* @param {(feature: Object) => void} map_on - поведение при нажатии на map
*/
set_map_on = recovery(
map_on => this.map_on = map_on,
(...e) => {
console.log("Ошибка при инициалиазции PopupManager.set_map_on");
e.forEach(er => {throw er});
}
)
}

123
lib/js/ref-manager.js Normal file
View File

@ -0,0 +1,123 @@
/**
* Удобное хранение и освобождение временных ссылок
* ВНИМАНИЕ: При очистке освобождаются все ссылки
*
* @class RefManager
* @property {Map<string, string>} refs - хранилище
*/
class RefManager {
/**
* @param {Map<string, string> | Object} [refs]
*/
constructor (refs = null) {
recovery((self) => {
/** Инициализация хранилища */
self.refs = new Map()
if(refs) {
if(Array.isArray(refs))
refs.forEach(ref => { self.save({ref}) });
else {
/** Связки из аргумента refs */
const refs_entries = (refs instanceof Map)
? refs.entries()
: (typeof refs === 'object' && refs !== null)
? Object.entries(refs)
: (() => {throw new Error("Невалидный аргумент")})()
/** Сохранение ссылок в хранилище */
Array.from(refs_entries).forEach(
([key, ref]) => self.save({key, ref})
)
}
}
}, (...e) => {
console.log("Ошибка инициализии RefManager");
e.forEach(er => {throw er});
}) (this)
}
/**
* Проверка ссылки
* @static @method
* @param {string} ref - ссылка
* @returns {string}
*/
static check = recovery(ref => {
if(typeof ref !== 'string')
throw Error("Неизвестный объект в массиве строк")
return ref
}, (...e) => {
console.log("Ошибка при проверке значения (RefManager.check)");
e.forEach(er => {throw er});
})
/**
* Сохранение сслыки
* @method
* @param {string} [key] - ключ
* @param {string} ref - ссылка
* @returns {{key: string, ref: string}}
*/
save = recovery(
({key = null, ref}) => {
var now_key = key
? key
: (Date.now() + Math.random()).toString(36)
this.refs.set(RefManager.check(now_key), RefManager.check(ref))
return {key: now_key, ref}
},
(...e) => {
console.log("Ошибка при сохранении (save)");
e.forEach(er => {throw er});
}
)
/**
* Получение ссылки по ключу
* @method
* @param {string} key - ключ
* @returns {string}
*/
get = recovery(
key => {
return this.refs.get(RefManager.check(key))
},
(...e) => {
console.log("Ошибка при извлечении ссылки (get)");
e.forEach(er => {throw er});
}
)
/**
* Очищает все сохранённые ссылки и освобождает ресурсы
* @method
*/
clear = recovery(
() => {
Array.from(this.refs.values()).forEach(URL.revokeObjectURL);
this.refs.clear()
},
(...e) => {
console.log("Ошибка при очищении (clear)");
e.forEach(er => {throw er});
}
)
/**
* Возвращает сконвертированную в объект список
* @method
* @returns {Object}
*/
get_all = recovery(
() => {
return Object.fromEntries(this.refs)
},
(...e) => {
console.log("Ошибка при конвертации и получени (get_all)");
e.forEach(er => {throw er});
}
)
}

View File

@ -0,0 +1,61 @@
/**
* @typedef {import('./ref-manager.js').RefManager} RefManager
*/
/**
* Контроль за одним интерактивным целевым слоем.
*
* @class SingleLayerManager
* @property {ol.layer.Vector} layer - тот самый single слой
*/
class SingleLayerManager {
/**
* @param {ol.layer.Vector } layer - ol-слой
*/
constructor (layer) {
recovery(self => {
/** Инъекция слоя */
if(layer instanceof ol.layer.Vector || layer instanceof ol.layer.VectorImage)
self.layer = layer
else
throw new Error("Несоответствующий тип у слоя-аргумента")
}, (...e) => {
console.log("Ошибка инициализии SingleLayerManager");
e.forEach(er => {throw er});
})(this)
}
/**
* Меняет источник целевого слоя.
* @method
* @param {object} json - необработанный json
*/
set = recovery(
json => {
const features = new ol.format.GeoJSON().readFeatures(json, {
featureProjection: 'EPSG:3857'
})
this.layer.getSource().clear()
this.layer.getSource().addFeatures(features)
},
(...e) => {
console.log("Ошибка изменения слоя (singleLayerManager.set)");
e.forEach(er => {throw er});
}
)
/**
* Очистка и удаление самого менеджера
* @method
*/
exit = recovery(
() => {
this.layer = null
},
(...e) => {
console.log("Ошибка изменения слоя (singleLayerManager.set)");
e.forEach(er => {throw er});
}
)
}

144
lib/js/slider.js Normal file
View File

@ -0,0 +1,144 @@
/**
* @typedef {Object} SliderArg
* @property {string} next_ref - сслыка на иконку вперед
* @property {string} previous_ref - ссылка на иконку назадъ
* @property {{main: HTMLStyleElement, next: HTMLElement, previous: HTMLElement}} slider - разграниченный слайдер
* @property {Array<string>} images - массив ссылок
*/
/**
* Класс для создания слайдера
* @class
* @property {HTMLElement} main - DOM-элемент для слайдов
* @property {number} current_slide - текущий слайд
* @property {Array<HTMLElement>} slides - слайды
*/
class Slider {
/**
* @param {SliderArg} arg
*/
constructor ({slider: {main, next, previous}, next_ref, previous_ref, images = null}) {
/** Безопасный вызов */
recovery(
self => {
/** Инъекция DOM-элементов (слайдер) */
Object.entries({main, next, previous}).forEach(([name, value]) => {
if (value instanceof HTMLElement)
self[name] = value
else throw new Error(`Невалидный аргумент ${value}`)
})
/** Инициалиазация кнопок */
Object.entries({
[next_ref]: {
element: next,
func: this.next_slide
},
[previous_ref]: {
element: previous,
func: this.previous_slide
}
}).forEach(
([ref, {element, func}]) => {
element.src = ref
element.onclick = func.bind(self)
}
)
self.current_slide = 0
self.slides = []
/** Инициализация фыфок (з****лся эти JSDoc'и писать)*/
if(Array.isArray(images))
images.forEach(self.add_slide)
},
(...e) => {
console.log("Ошибка при инициалиазции slider-page");
e.forEach(er => {throw er});
}
) (this)
}
/**
* Обновляет слайдер (смещает на 80vw * current_slide)
* @method
*/
update_slider = recovery(
() => this.main.style.transform = `translateX(-${this.current_slide * 80}vw)`,
(...e) => {
console.log("Ошибка при обновлении слайдера (Slider.update_slider)")
e.forEach(er => {throw er})
}
)
/**
* Переключает на следующий слайд
* @method
*/
next_slide = recovery(
() => {
if (this.current_slide < this.slides.length - 1)
this.current_slide++
else
this.current_slide = 0
this.update_slider()
},
(...e) => {
console.log("Ошибка при переключении на следующий слайд")
e.forEach(er => {throw er})
}
)
/**
* Переключает на предыдущий слайд
* @method
*/
previous_slide = recovery(
() => {
if (this.current_slide > 0)
this.current_slide--
else
this.current_slide = this.slides.length - 1
this.update_slider()
},
(...e) => {
console.log("Ошибка при переключении на следующий слайд")
e.forEach(er => {throw er})
}
)
/**
* Добавление новых слайдов
* @method
*/
add_slide = recovery(
slide_ref => {
/** Изображение */
const img = document.createElement('img')
/** Применение стилей */
img.classList.add('slide')
/** Линковка ссылки */
img.src = slide_ref
/** Регистрация слайда в dom-элементе */
this.main.appendChild(img)
/** Регистрация в массиве */
this.slides.push(img)
},
(...e) => {
console.log("Ошибка при добавлении слайда")
e.forEach(er => {throw er})
}
)
clear = recovery(
() => {
this.main.innerHTML = ''
},
(...e) => {
console.log("Ошибка при очищении")
e.forEach(er => {throw er})
}
)
}

354
lib/ol/ol.css Normal file
View File

@ -0,0 +1,354 @@
:root,
:host {
--ol-background-color: white;
--ol-accent-background-color: #F5F5F5;
--ol-subtle-background-color: rgba(128, 128, 128, 0.25);
--ol-partial-background-color: rgba(255, 255, 255, 0.75);
--ol-foreground-color: #333333;
--ol-subtle-foreground-color: #666666;
--ol-brand-color: #00AAFF;
}
.ol-box {
box-sizing: border-box;
border-radius: 2px;
border: 1.5px solid var(--ol-background-color);
background-color: var(--ol-partial-background-color);
}
.ol-mouse-position {
top: 8px;
right: 8px;
position: absolute;
}
.ol-scale-line {
background: var(--ol-partial-background-color);
border-radius: 4px;
bottom: 8px;
left: 8px;
padding: 2px;
position: absolute;
}
.ol-scale-line-inner {
border: 1px solid var(--ol-subtle-foreground-color);
border-top: none;
color: var(--ol-foreground-color);
font-size: 10px;
text-align: center;
margin: 1px;
will-change: contents, width;
transition: all 0.25s;
}
.ol-scale-bar {
position: absolute;
bottom: 8px;
left: 8px;
}
.ol-scale-bar-inner {
display: flex;
}
.ol-scale-step-marker {
width: 1px;
height: 15px;
background-color: var(--ol-foreground-color);
float: right;
z-index: 10;
}
.ol-scale-step-text {
position: absolute;
bottom: -5px;
font-size: 10px;
z-index: 11;
color: var(--ol-foreground-color);
text-shadow: -1.5px 0 var(--ol-partial-background-color), 0 1.5px var(--ol-partial-background-color), 1.5px 0 var(--ol-partial-background-color), 0 -1.5px var(--ol-partial-background-color);
}
.ol-scale-text {
position: absolute;
font-size: 12px;
text-align: center;
bottom: 25px;
color: var(--ol-foreground-color);
text-shadow: -1.5px 0 var(--ol-partial-background-color), 0 1.5px var(--ol-partial-background-color), 1.5px 0 var(--ol-partial-background-color), 0 -1.5px var(--ol-partial-background-color);
}
.ol-scale-singlebar {
position: relative;
height: 10px;
z-index: 9;
box-sizing: border-box;
border: 1px solid var(--ol-foreground-color);
}
.ol-scale-singlebar-even {
background-color: var(--ol-subtle-foreground-color);
}
.ol-scale-singlebar-odd {
background-color: var(--ol-background-color);
}
.ol-unsupported {
display: none;
}
.ol-viewport,
.ol-unselectable {
-webkit-touch-callout: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
}
.ol-viewport canvas {
all: unset;
overflow: hidden;
}
.ol-viewport {
touch-action: pan-x pan-y;
}
.ol-selectable {
-webkit-touch-callout: default;
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
}
.ol-grabbing {
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
.ol-grab {
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.ol-control {
position: absolute;
background-color: var(--ol-subtle-background-color);
border-radius: 4px;
}
.ol-zoom {
top: .5em;
left: .5em;
}
.ol-rotate {
top: .5em;
right: .5em;
transition: opacity .25s linear, visibility 0s linear;
}
.ol-rotate.ol-hidden {
opacity: 0;
visibility: hidden;
transition: opacity .25s linear, visibility 0s linear .25s;
}
.ol-zoom-extent {
top: 4.643em;
left: .5em;
}
.ol-full-screen {
right: .5em;
top: .5em;
}
.ol-control button {
display: block;
margin: 1px;
padding: 0;
color: var(--ol-subtle-foreground-color);
font-weight: bold;
text-decoration: none;
font-size: inherit;
text-align: center;
height: 1.375em;
width: 1.375em;
line-height: .4em;
background-color: var(--ol-background-color);
border: none;
border-radius: 2px;
}
.ol-control button::-moz-focus-inner {
border: none;
padding: 0;
}
.ol-zoom-extent button {
line-height: 1.4em;
}
.ol-compass {
display: block;
font-weight: normal;
will-change: transform;
}
.ol-touch .ol-control button {
font-size: 1.5em;
}
.ol-touch .ol-zoom-extent {
top: 5.5em;
}
.ol-control button:hover,
.ol-control button:focus {
text-decoration: none;
outline: 1px solid var(--ol-subtle-foreground-color);
color: var(--ol-foreground-color);
}
.ol-zoom .ol-zoom-in {
border-radius: 2px 2px 0 0;
}
.ol-zoom .ol-zoom-out {
border-radius: 0 0 2px 2px;
}
.ol-attribution {
text-align: right;
bottom: .5em;
right: .5em;
max-width: calc(100% - 1.3em);
display: flex;
flex-flow: row-reverse;
align-items: center;
}
.ol-attribution a {
color: var(--ol-subtle-foreground-color);
text-decoration: none;
}
.ol-attribution ul {
margin: 0;
padding: 1px .5em;
color: var(--ol-foreground-color);
text-shadow: 0 0 2px var(--ol-background-color);
font-size: 12px;
}
.ol-attribution li {
display: inline;
list-style: none;
}
.ol-attribution li:not(:last-child):after {
content: " ";
}
.ol-attribution img {
max-height: 2em;
max-width: inherit;
vertical-align: middle;
}
.ol-attribution button {
flex-shrink: 0;
}
.ol-attribution.ol-collapsed ul {
display: none;
}
.ol-attribution:not(.ol-collapsed) {
background: var(--ol-partial-background-color);
}
.ol-attribution.ol-uncollapsible {
bottom: 0;
right: 0;
border-radius: 4px 0 0;
}
.ol-attribution.ol-uncollapsible img {
margin-top: -.2em;
max-height: 1.6em;
}
.ol-attribution.ol-uncollapsible button {
display: none;
}
.ol-zoomslider {
top: 4.5em;
left: .5em;
height: 200px;
}
.ol-zoomslider button {
position: relative;
height: 10px;
}
.ol-touch .ol-zoomslider {
top: 5.5em;
}
.ol-overviewmap {
left: 0.5em;
bottom: 0.5em;
}
.ol-overviewmap.ol-uncollapsible {
bottom: 0;
left: 0;
border-radius: 0 4px 0 0;
}
.ol-overviewmap .ol-overviewmap-map,
.ol-overviewmap button {
display: block;
}
.ol-overviewmap .ol-overviewmap-map {
border: 1px solid var(--ol-subtle-foreground-color);
height: 150px;
width: 150px;
}
.ol-overviewmap:not(.ol-collapsed) button {
bottom: 0;
left: 0;
position: absolute;
}
.ol-overviewmap.ol-collapsed .ol-overviewmap-map,
.ol-overviewmap.ol-uncollapsible button {
display: none;
}
.ol-overviewmap:not(.ol-collapsed) {
background: var(--ol-subtle-background-color);
}
.ol-overviewmap-box {
border: 1.5px dotted var(--ol-subtle-foreground-color);
}
.ol-overviewmap .ol-overviewmap-box:hover {
cursor: move;
}
.ol-overviewmap .ol-viewport:hover {
cursor: pointer;
}

2
lib/ol/ol.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,46 @@
<?php
namespace Catcher;
use Throwable;
/**
* Вызов обработчика ошибок с защитой от повторных исключений.
*
* @param callable $restorer Функция восстановления, принимающая исключения.
* @param Throwable ...$errors Исключения, переданные для обработки.
* @return mixed
* @throws Throwable
*/
function start_restorer(callable $restorer, Throwable ...$errors) {
try {
return $restorer(...$errors);
} catch (Throwable $e) {
error_log("Restorer error: " . $e->getMessage());
throw $e;
}
}
/**
* Оборачивает функцию и добавляет обработку ошибок.
*
* @param callable $func Основная функция.
* @param callable|null $restorer Обработчик ошибок (по умолчанию пробрасывает исключение).
* @return callable
*/
function recovery(callable $func, callable $restorer = null): callable {
if ($restorer === null) {
$restorer = function (Throwable ...$errors) {
throw $errors[0]; // Пробрасываем первое исключение
};
}
return function (...$args) use ($func, $restorer) {
try {
return $func(...$args);
} catch (Throwable $e) {
return start_restorer($restorer, $e);
}
};
}
?>

View File

@ -0,0 +1,90 @@
<?php
namespace DataBaseManager\DBManager;
use DataBaseManager\Entitie\Entitie;
use PDO;
use InvalidArgumentException;
class DBManager
{
private $pdo;
private $entitie;
public function __construct(Entitie $entitie, object $config)
{
$this->entitie = $entitie;
$dsn = "{$config->driver}:host={$config->host};port={$config->port};dbname={$config->dbname}";
$username = $config->username;
$password = $config->password;
$options = isset($config->options) ? (array)$config->options : [];
$this->pdo = new PDO($dsn, $username, $password, $options);
}
public function create_table()
{
return $this->pdo->query($this->entitie->get_create_table());
}
public function delete_table()
{
return $this->pdo->query($this->entitie->get_delete_table());
}
public function create(array $params)
{
$stmt = $this->pdo->prepare($this->entitie->get_create());
return $stmt->execute($params);
}
public function select(array $params)
{
$sql = str_replace("*", $params[0], $this->entitie->get_select());
array_shift($params);
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function select_all(array $params = ["*"])
{
$sql = str_replace("*", $params[0], $this->entitie->get_select_all());
$stmt = $this->pdo->prepare($sql);
$stmt->execute([]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function select_by_condition(array $params)
{
$sql = str_replace("*", $params[0], $this->entitie->get_select_all());
array_shift($params);
$stmt = $this->pdo->prepare($sql . ' WHERE ' . $params[0]);
$stmt->execute([]);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function remove(array $params)
{
$stmt = $this->pdo->prepare($this->entitie->get_remove());
return $stmt->execute($params);
}
public function clear(array $params)
{
$stmt = $this->pdo->prepare($this->entitie->get_clear());
return $stmt->execute($params);
}
public function create_all(array $params)
{
$create_all = $this->entitie->get_create_all();
$stmt = $this->pdo->prepare($create_all(sizeof($params)));
$params_merge = array_merge(...$params);
return $stmt->execute($params_merge);
}
public function update(array $params)
{
$update = $this->entitie->get_update();
$stmt = $this->pdo->prepare($update($params[0]));
array_shift($params);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function update_by_condition(array $params)
{
$update = $this->entitie->get_update_by_condition();
$stmt = $this->pdo->prepare($update($params[0]));
array_shift($params);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace DataBaseManager\Entitie;
class Entitie
{
private array $column_array;
private string $table_name;
private string $create_table;
private string $delete_table;
private string $type_id;
private string $create;
private string $select;
private string $select_all;
private string $remove;
private string $clear;
private \Closure $create_all;
private \Closure $update;
private \Closure $update_by_condition;
public function __construct(array $column_array, string $table_name, string $type_id = "serial")
{
$this->column_array = $column_array;
$this->table_name = $table_name;
$this->type_id = $type_id;
$columns_sql = implode(", ", $this->column_array);
$column_names = implode(", ", array_map(fn($col) => explode(" ", $col)[0], $this->column_array));
$placeholders = implode(", ", array_fill(0, count($this->column_array), "?"));
$set_clause = implode(", ", array_map(fn($col) => explode(" ", $col)[0] . "=?", $this->column_array));
$this->create_table = "CREATE TABLE IF NOT EXISTS {$this->table_name} (id {$this->type_id}, {$columns_sql})";
$this->delete_table = "DROP TABLE IF EXISTS {$this->table_name}";
$this->create = "INSERT INTO {$this->table_name} ({$column_names}) VALUES ({$placeholders})";
$this->select = "SELECT * FROM {$this->table_name} WHERE id=?";
$this->select_all = "SELECT * FROM {$this->table_name}";
$this->remove = "DELETE FROM {$this->table_name} WHERE id=?";
$this->clear = "TRUNCATE TABLE {$this->table_name}";
$this->create_all = function($length) use ($placeholders, $column_names, $table_name) {
$placeholders_array = implode(", ", array_fill(0, $length, "({$placeholders})"));
return "INSERT INTO {$table_name} ({$column_names}) VALUES {$placeholders_array}";
};
$this->update = fn($id) => "UPDATE {$this->table_name} SET {$set_clause} WHERE id = ?";
$this->update_by_condition = fn($condition) => "UPDATE {$this->table_name} SET {$set_clause} WHERE {$condition}";
}
public function get_create_table(): string { return $this->create_table; }
public function get_delete_table(): string { return $this->delete_table; }
public function get_create(): string { return $this->create; }
public function get_select(): string { return $this->select; }
public function get_select_all(): string { return $this->select_all; }
public function get_remove(): string { return $this->remove; }
public function get_clear(): string { return $this->clear; }
public function get_create_all(): \Closure { return $this->create_all; }
public function get_update(): \Closure { return $this->update; }
public function get_update_by_condition(): \Closure { return $this->update_by_condition; }
}