import '../scss/MesseMap.scss';
import MapProviders from './utils/MapProviders.js';
import ScrollBar from './utils/ScrollBar.js';
import './utils/SmoothWheelZoom.js';
const { jsPDF } = window.jspdf;
const CONST = {
VERSION: '1.1.0',
DEBUG: false
};
/**
* @class
* @constructor
* @public
**/
class MesseMap {
/**
* @summary The MesseMap main component
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* The MesseMap class is made to handle the whole application, its interactivity
* and all its user events. It hold the code to generate the poster output and serve
* it to the user. (see README.md for further details about used libraries). It also
* handle the data exchange with server to store saved posters as JSON.
* This constructor will initialize the Leaflet map and all its manipulators and will
* then listen to user events for text and export settings.
* </blockquote>
**/
constructor() {
if (CONST.DEBUG) { console.log('MesseMap constructor called'); }
/**
* The core Leaflet.js map
* @type {Object}
* @private
**/
this._map = null;
/**
* The Leaflet control search object
* @type {Object}
* @private
**/
this._search = null;
/**
* The flag to ensure all tiles are loaded before printing canvas to image
* @type {Boolean}
* @private
**/
this._tilesLoaded = false;
/**
* The flag to ensure the comment can be updated with center lat/lng
* @type {Boolean}
* @private
**/
this._commentEdited = false;
/**
* setInterval ID used to frequently ask for printing (only if tiles are loaded)
* @type {Number}
* @private
**/
this._intervalId = -1;
/**
* Hold default or saved theme colors (light/dark)
* @type {Object}
* @private
**/
this._cssTheme = {
lbg: '#FFFFFE',
ltxt: '#000001',
lcom: '#999998',
dbg: '#000001',
dtxt: '#FFFFFE',
dcom: '#999998'
};
/**
* The currently applied language
* @type {String}
* @private
**/
this._lang = localStorage.getItem('lang') || 'en';
/**
* The nls file that holds language key values
* @type {Object}
* @private
**/
this._nls = {};
/**
* The aside scrollbar component
* @type {Object}
* @private
**/
this._scroll = null;
this._data = {
orientation: 'vertical',
style: 'standard',
darkTheme: false,
upText: false,
layer: 'Imagery (E)',
hasIcon: false,
icon: {
color: '#FFFFFF',
image: '/assets/img/icon/home.svg',
size: 2, // Size in rem
x: 48.33, // Size in %
y: 48.33 // Size in %
}
};
// Begin the initialization sequence (interface and events)
this._initInterface()
.then(this._initMap.bind(this))
.then(this._initEvents.bind(this))
.then(this._initAdvancedFeatures.bind(this))
.then(this._endStartup.bind(this))
.catch(error => console.error(error));
}
// ======================================================================= //
// ----------------- Application initialization sequence ----------------- //
// ======================================================================= //
/**
* @method
* @name _initInterface
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since November 2022
* @description
* <blockquote>
* This method will fetch the proper lang file and then will update the interface accordingly
* </blockquote>
* @returns {Promise} A resolved or rejected Promise
**/
_initInterface() {
if (CONST.DEBUG) { console.log('MesseMap._initInterface() called'); }
return new Promise((resolve, reject) => {
// Get lang from storage, or use default
if (!localStorage.getItem('lang')) {
localStorage.setItem('lang', this._lang);
}
// Then fetch and update UI with proper language
fetch(`assets/nls/${this._lang}.json`).then(data => {
data.text().then(lang => {
document.documentElement.setAttribute('lang', this._lang); // Update HTML lang tag
this._nls = JSON.parse(lang); // Save language keys/values
// Update head content with translations
document.title = this._nls.pageTitle;
document.querySelector('meta[name="description"]').setAttribute('content', this._nls.pageDescription);
document.querySelector('meta[property="og:description"]').setAttribute('content', this._nls.pageDescription);
document.querySelector('meta[name="twitter:description"]').setAttribute('content', this._nls.pageDescription);
// Update page content with translations
this.replaceString(document.body, '{{TITLE}}', this._nls.title);
this.replaceString(document.body, '{{HELPER}}', this._nls.helper);
this.replaceString(document.body, '{{STYLE}}', this._nls.style.title);
this.replaceString(document.body, '{{MAP_ORIENTATION}}', this._nls.style.orientation);
this.replaceString(document.body, '{{VERTICAL}}', this._nls.style.vertical);
this.replaceString(document.body, '{{HORIZONTAL}}', this._nls.style.horizontal);
this.replaceString(document.body, '{{MAP_STYLE}}', this._nls.style.mapStyle);
this.replaceString(document.body, '{{STYLE_STD}}', this._nls.style.std);
this.replaceString(document.body, '{{STYLE_TRAVEL}}', this._nls.style.travel);
this.replaceString(document.body, '{{STYLE_FRAME}}', this._nls.style.frame);
this.replaceString(document.body, '{{STYLE_PURE}}', this._nls.style.pure);
this.replaceString(document.body, '{{STYLE_PANTONE}}', this._nls.style.pantone);
this.replaceString(document.body, '{{STYLE_MAP}}', this._nls.style.map);
this.replaceString(document.body, '{{STYLE_WINDOW}}', this._nls.style.window);
this.replaceString(document.body, '{{STYLE_AIR}}', this._nls.style.air);
this.replaceString(document.body, '{{STYLE_HIPSTER}}', this._nls.style.hipster);
this.replaceString(document.body, '{{DARK_THEME}}', this._nls.style.darkTheme);
this.replaceString(document.body, '{{UP_TEXT}}', this._nls.style.upText);
this.replaceString(document.body, '{{TEXT}}', this._nls.text.title);
this.replaceString(document.body, '{{MAP_TITLE}}', this._nls.text.mapTitle);
this.replaceString(document.body, '{{MAP_TITLE_PLACEHOLDER}}', this._nls.text.mapTitlePlaceholder);
this.replaceString(document.body, '{{MAP_SUBTITLE}}', this._nls.text.mapSubtitle);
this.replaceString(document.body, '{{MAP_SUBTITLE_PLACEHOLDER}}', this._nls.text.mapSubtitlePlaceholder);
this.replaceString(document.body, '{{MAP_COMMENT}}', this._nls.text.mapComment);
this.replaceString(document.body, '{{MAP_COMMENT_PLACEHOLDER}}', this._nls.text.mapCommentPlaceholder);
this.replaceString(document.body, '{{ICON}}', this._nls.icon.title);
this.replaceString(document.body, '{{TOGGLE_ICON}}', this._nls.icon.toggle);
this.replaceString(document.body, '{{ICON_SIZE}}', this._nls.icon.size);
this.replaceString(document.body, '{{ICON_COLOR}}', this._nls.icon.color);
this.replaceString(document.body, '{{ICON_IMAGE}}', this._nls.icon.image);
this.replaceString(document.body, '{{EXPORT}}', this._nls.export.title);
this.replaceString(document.body, '{{EXPORT_DIMENSION}}', this._nls.export.dimension);
this.replaceString(document.body, '{{EXPORT_AT}}', this._nls.export.at);
this.replaceString(document.body, '{{EXPORT_FORMAT}}', this._nls.export.format);
this.replaceString(document.body, '{{EXPORT_BUTTON}}', this._nls.export.button);
this.replaceString(document.body, '{{CREDITS}}', this._nls.export.credits);
resolve();
}).catch(reject);
}).catch(reject);
});
}
/**
* @method
* @name _initMap
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* The <code>_initInterface()</code> method will create the Leaflet map and insert it to the DOM.
* It will then handle the supported layer (in MapProviders) and will finally update
* text in poster according to input default values.
* It returns a promise that is resolved when interface is initialized, or that is
* rejected there is no Leaflet in the user session (fatal error).
* </blockquote>
* @returns {Promise} A resolved or rejected Promise
**/
_initMap() {
if (CONST.DEBUG) { console.log('MesseMap._initMap() called'); }
return new Promise((resolve, reject) => {
try {
// Use #map div to inject Leaflet in, use SmoothWheelZoom flags
this._map = window.L.map('map', {
attributionControl: false,
zoomSnap: 0, // On resize, all fitBounds to precisely be set
scrollWheelZoom: false, // SmoothWheelZoom lib
smoothWheelZoom: true, // SmoothWheelZoom lib
smoothSensitivity: 1, // SmoothWheelZoom lib
}).setView([44.79777779831652, 1.542703666063447], 5);
// Search control creation
this._search = new window.L.Control.Search({
url: 'https://nominatim.openstreetmap.org/search?format=json&q={s}',
jsonpParam: 'json_callback',
propertyName: 'display_name',
propertyLoc: ['lat', 'lon'],
marker: false,
autoCollapse: true,
firstTipSubmit: true,
textPlaceholder: this._nls.search.placeholder,
textCancel: this._nls.search.cancel,
textErr: this._nls.search.error
});
// Update map bounds
this._map.setMaxBounds(window.L.latLngBounds(
window.L.latLng(-89.98155760646617, -360),
window.L.latLng(89.99346179538875, 360)
));
} catch (error) {
// The only error case is Leaflet doesn't exist here
reject(error);
return;
}
// Add default layer in map
MapProviders.layers['Imagery (E)'].addTo(this._map);
// Add layer switch radio on bottom right of the map
window.L.control.layers(MapProviders.layers, MapProviders.overlays, { position: 'topright' }).addTo(this._map);
// Add search command
this._map.addControl(this._search);
// Apply default input text to poster, empty object
this._applyTexts({});
// Load user theme overrides
const cssTheme = localStorage.getItem('theme');
if (cssTheme && JSON.parse(cssTheme)) {
this._cssTheme = JSON.parse(cssTheme);
this.applyThemeColor();
}
// Build scrollbar for aside
this._scroll = new ScrollBar({
target: document.getElementById('scrollable-aside'),
style: {
color: '#56d45b',
size: '8px',
radius: '.333rem',
lowOpacity: '0.5',
highOpacity: '1',
transitionDuration: '0.1s'
}
});
resolve();
});
}
/**
* @method
* @name _initEvents
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* The <code>_initEvents()</code> method will listen to all required events to manipulate the map
* and to modify the poster style and texts. Those events.
* It returns a promise that is resolved when interface is initialized. There is no reject case.
* </blockquote>
* @returns {Promise} A resolved Promise
**/
_initEvents() {
if (CONST.DEBUG) { console.log('MesseMap._initEvents() called'); }
return new Promise(resolve => {
// Style
document.getElementById('toggle-style').addEventListener('click', this._toggleCategory.bind(this));
const orientations = document.getElementById('toggle-orientation-container');
for (let i = 0; i < orientations.children.length; ++i) {
orientations.children[i].addEventListener('click', this._updateMapOrientation.bind(this));
}
const styles = document.getElementById('map-style');
for (let i = 0; i < styles.children.length; ++i) {
styles.children[i].addEventListener('click', this._updateMapStyle.bind(this));
}
document.getElementById('dark-theme').addEventListener('change', this._updateDarkTheme.bind(this));
document.getElementById('txt-position').addEventListener('change', this._updateTextPosition.bind(this));
document.getElementById('theme-editor').addEventListener('click', this._themeEditModal.bind(this));
// Text modification events (color, style etc.)
document.getElementById('toggle-texts').addEventListener('click', this._toggleCategory.bind(this));
document.getElementById('title-color').addEventListener('input', this._textColorEdit.bind(this));
document.getElementById('subtitle-color').addEventListener('input', this._textColorEdit.bind(this));
document.getElementById('comment-color').addEventListener('input', this._textColorEdit.bind(this));
document.getElementById('user-title').addEventListener('input', this._applyTexts.bind(this));
document.getElementById('user-subtitle').addEventListener('input', this._applyTexts.bind(this));
document.getElementById('user-comment').addEventListener('input', this._applyTexts.bind(this));
// Icon
document.getElementById('toggle-icon').addEventListener('click', this._toggleCategory.bind(this));
document.getElementById('activate-icon').addEventListener('change', this._toggleIcon.bind(this));
document.getElementById('icon-size').addEventListener('input', this._updateIconSize.bind(this));
this._handleIconDrag();
document.getElementById('icon-color').addEventListener('input', this._updateIconColor.bind(this));
const icons = document.getElementById('icon-images');
for (let i = 0; i < icons.children.length; ++i) {
icons.children[i].addEventListener('click', this._updateIconImage.bind(this));
}
// Export settings
document.getElementById('toggle-export').addEventListener('click', this._toggleCategory.bind(this));
document.getElementById('image-width').addEventListener('input', this._updateDimensionLabel.bind(this));
document.getElementById('map-save').addEventListener('click', this._download.bind(this));
// Update selection buttons
const sizes = document.getElementById('size-container');
for (let i = 0; i < sizes.children.length; ++i) {
sizes.children[i].addEventListener('click', this._updateDimensionClicked.bind(this));
}
// Listening to close modal event
document.getElementById('modal-overlay').addEventListener('click', this._closeModal.bind(this));
document.getElementById('credit-modal').addEventListener('click', this._creditModal.bind(this));
// Load event on map layers for loaded tiles (to ensure the printing occurs with all map tiles)
for (const layer in MapProviders.layers) {
MapProviders.layers[layer].on('load', () => this._tilesLoaded = true);
}
this._map.on('move', this._updateCommentLabel.bind(this));
this._map.on('baselayerchange', e => {
this._data.layer = e.name;
});
this._search.on('search:locationfound', this._searchMatch.bind(this));
resolve();
});
}
/**
* @method
* @name _initAdvancedFeatures
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since November 2023
* @description
* <blockquote>
* In case of url contains <code>?callmeroot()</code>, append advanced inputs and controls
* It returns a promise that is resolved when interface is initialized. There is no reject case.
* </blockquote>
* @returns {Promise} A resolved Promise
**/
_initAdvancedFeatures() {
if (CONST.DEBUG) { console.log('MesseMap._initAdvancedFeatures called'); }
return new Promise(resolve => {
if (window.location.href.indexOf('?callmeroot') > -1) {
const reader = new FileReader();
const importer = document.createElement('INPUT');
importer.type = 'file';
importer.accept = '.json';
reader.addEventListener('load', rawData => {
const data = JSON.parse(rawData.target.result);
this._setPosterConfigurationFromData(data);
});
importer.addEventListener('change', () => {
if (importer.files.length) {
reader.readAsText(importer.files[0]);
}
});
document.body.appendChild(importer);
}
resolve();
});
}
/**
* @method
* @name _endStartup
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* This method will hide the print-overlay, and initialize its values
* for later exports after splash screen.
* </blockquote>
**/
_endStartup() {
if (CONST.DEBUG) { console.log('MesseMap._endStartup called'); }
return new Promise(resolve => {
this.unblockInterface().then(resolve);
});
}
// ======================================================================= //
// ----------------------- Input events callbacks ------------------------ //
// ======================================================================= //
/**
* @method
* @name _setPosterConfigurationFromData
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since November 2023
* @description
* <blockquote>
* From a JSON input, apply any transformation to the map or the poster, updates
* colors, texts, map style, map orientation, dark theme, text position and position
* map at lat/lng and zoom level described in saved file.
* </blockquote>
* @param {Object} data - A JSON map object, described in /saved/README.md
**/
_setPosterConfigurationFromData(data) {
// Update texts
document.getElementById('title').innerHTML = data.text.title;
document.getElementById('subtitle').innerHTML = data.text.subtitle;
document.getElementById('comment').innerHTML = data.text.comment;
// Apply poster orientation
const orientations = document.getElementById('toggle-orientation-container');
for (let i = 0; i < orientations.children.length; ++i) {
if (orientations.children[i].dataset.orientation === data.style.orientation) {
orientations.children[i].click();
break;
}
}
// Apply dark theme if not already set and checked in saved JSON
if (data.style.darkTheme && !document.getElementById('dark-theme').checked || !data.style.darkTheme && document.getElementById('dark-theme').checked) {
document.getElementById('dark-theme').click();
}
// Modify upText only if not already toggle and checked in saved JSON
if (data.style.upText && !document.getElementById('txt-position').checked || !data.style.upText && document.getElementById('txt-position').checked) {
document.getElementById('txt-position').click();
}
// Update text and theme colors
this._cssTheme.lbg = data.style.colors.lbg;
this._cssTheme.ltxt = data.style.colors.ltxt;
this._cssTheme.lcom = data.style.colors.lcom;
this._cssTheme.dbg = data.style.colors.dbg;
this._cssTheme.dtxt = data.style.colors.dtxt;
this._cssTheme.dcom = data.style.colors.dcom;
this.applyThemeColor();
// Update map style
const styles = document.getElementById('map-style');
for (let i = 0; i < styles.children.length; ++i) {
if (styles.children[i].dataset.style === data.style.style) {
styles.children[i].click();
break;
}
}
if (data.icon && data.icon.displayed === true) {
document.getElementById('activate-icon').click();
document.getElementById('icon-size').value = data.icon.size;
this._updateIconSize({
target: document.getElementById('icon-size')
});
const icons = document.getElementById('icon-images');
for (let i = 0; i < icons.children.length; ++i) {
if (icons.children[i].dataset.url === data.icon.image) {
icons.children[i].click();
break;
}
}
document.getElementById('icon-color').value = data.icon.color;
this._updateIconColor({
target: document.getElementById('icon-color')
});
document.getElementById('map-icon').style.left = `${data.icon.x}%`;
document.getElementById('map-icon').style.top = `${data.icon.y}%`;
}
// Update map position
this._map.setView(new window.L.LatLng(data.map.center.lat, data.map.center.lng), data.map.zoom);
// Switch map base layer
this._map.removeLayer(MapProviders.layers[this._data.layer]);
this._map.addLayer(MapProviders.layers[data.map.layer]);
}
/**
* @method
* @name _toggleCategory
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since April 2023
* @description
* <blockquote>
* Expand or colapse a given map modifiers category
* </blockquote>
* @param {Event} e - The click event on the category expander/collapser
**/
_toggleCategory(e) {
if (CONST.DEBUG) { console.log('MesseMap._toggleCategory() called with ', e); }
const element = document.getElementById(e.target.dataset.id);
if (element) {
element.classList.toggle('expanded');
// Select the span inside h1 if not the one clicked
let expandCollapse = e.target;
if (expandCollapse.className !== 'toggle') {
expandCollapse = expandCollapse.lastElementChild;
}
// Update expand/collapse text
if (element.classList.contains('expanded')) {
expandCollapse.innerHTML = '▲';
} else {
expandCollapse.innerHTML = '▼';
}
this._scroll.updateScrollbar();
requestAnimationFrame(this._scroll.updateScrollbar.bind(this._scroll));
}
}
/**
* @method
* @name _updateMapOrientation
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Will update the poster vertical/horizontal orientation
* </blockquote>
* @param {Event} e - The click event on the theme checkbox
**/
_updateMapOrientation(e) {
if (CONST.DEBUG) { console.log('MesseMap._updateMapOrientation() called with ', e); }
let previousOrientation = '';
const orientations = document.getElementById('toggle-orientation-container');
for (let i = 0; i < orientations.children.length; ++i) {
if (orientations.children[i].classList.contains('selected')) {
previousOrientation = orientations.children[i].dataset.orientation;
orientations.children[i].classList.remove('selected');
break;
}
}
// Update menu, remove previous style from map and add new style to map
e.target.classList.add('selected');
document.getElementById('map-output').classList.remove(`${previousOrientation}`);
document.getElementById('map-output').classList.add(`${e.target.dataset.orientation}`);
this._data.orientation = e.target.dataset.orientation; // Update internal data
setTimeout(() => { // Transition all .2s avoidance
const bounds = this._map.getBounds(); // Map bound before scaling
this._map.invalidateSize();
this._map.fitBounds(bounds);
}, 200);
}
/**
* @method
* @name _updateDarkTheme
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Will update the poster with dark or light css theme colors. If colors have been overiden
* by user, they will properly applied to the poster.
* </blockquote>
* @param {Event} e - The change event on the theme checkbox
**/
_updateDarkTheme(e) {
if (CONST.DEBUG) { console.log('MesseMap._updateDarkTheme() called with ', e); }
if (e.target.checked) {
document.body.classList.remove('light-theme');
document.body.classList.add('dark-theme');
this._data.darkTheme = true;
} else {
document.body.classList.remove('dark-theme');
document.body.classList.add('light-theme');
this._data.darkTheme = false;
}
}
/**
* @method
* @name _updateTextPosition
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Will update the poster text position wether to be top or bottom
* </blockquote>
* @param {Event} e - The change event on the up text checkbox
**/
_updateTextPosition(e) {
if (CONST.DEBUG) { console.log('MesseMap._updateTextPosition() called with ', e); }
if (e.target.checked) {
document.getElementById('map-output').classList.add('txt-reverse');
this._data.upText = true;
} else {
document.getElementById('map-output').classList.remove('txt-reverse');
this._data.upText = false;
}
}
/**
* @method
* @name _updateMapStyle
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Updates the map style according to the element the user clicked on. All styles
* are handled in the scss style file.
* </blockquote>
* @param {Event} e - The click event on the style button
**/
_updateMapStyle(e) {
if (CONST.DEBUG) { console.log('MesseMap._updateMapStyle() called with ', e); }
let previousStyle = '';
const styles = document.getElementById('map-style');
for (let i = 0; i < styles.children.length; ++i) {
if (styles.children[i].classList.contains('selected')) {
previousStyle = styles.children[i].dataset.style;
styles.children[i].classList.remove('selected');
break;
}
}
// Update menu, remove previous style from map and add new style to map
e.target.classList.add('selected');
document.getElementById('map-output').classList.remove(`${previousStyle}-style`);
document.getElementById('map-output').classList.add(`${e.target.dataset.style}-style`);
this._data.style = e.target.dataset.style; // Update internal data
}
/**
* @method
* @name _applyTexts
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Simply apply the input text to the poster (for title, subtitle and comment)
* </blockquote>
* @param {Event} e - The text input
**/
_applyTexts(e) {
if (CONST.DEBUG) { console.log('MesseMap._applyTexts() called with ', e); }
document.getElementById('title').innerHTML = document.getElementById('user-title').value;
document.getElementById('subtitle').innerHTML = document.getElementById('user-subtitle').value;
document.getElementById('comment').innerHTML = document.getElementById('user-comment').value;
if (e && e.target && e.target.id === 'user-comment') {
this._commentEdited = true;
}
}
/**
* @method
* @name _textColorEdit
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Modify the text on map color. Overrides the default theme color
* </blockquote>
* @param {Event} e - The input color change
**/
_textColorEdit(e) {
if (CONST.DEBUG) { console.log('MesseMap._textColorEdit() called with ', e); }
document.getElementById(e.target.dataset.type).style.color = e.target.value;
}
/**
* @method
* @name _updateCommentLabel
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Update the comment with map center coordinates, only if user didn't set a text in comment
* </blockquote>
**/
_updateCommentLabel() {
if (CONST.DEBUG) { console.log('MesseMap._updateCommentLabel() called'); }
if (!this._commentEdited) {
const c = this._map.getCenter();
document.getElementById('comment').innerHTML = `${this.precisionRound(c.lat % 90, 3)}°N / ${this.precisionRound(c.lng % 180, 3)}° E`;
}
}
/**
* @method
* @name _toggleIcon
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since January 2024
* @description
* <blockquote>
* Display or hide the icon on the output map
* </blockquote>
* @param {Event} e - The check input
**/
_toggleIcon(e) {
if (CONST.DEBUG) { console.log('MesseMap._toggleIcon() called with ', e); }
const icon = document.getElementById('map-icon');
if (e.target.checked) {
// Init icon position
icon.style.left = `${this._data.icon.x}%`;
icon.style.top = `${this._data.icon.y}%`;
icon.classList.add('visible');
icon.style.backgroundColor = this._data.icon.color;
icon.style.mask = `url(${this._data.icon.image}) no-repeat center / contain`;
this._data.hasIcon = true;
} else {
icon.classList.remove('visible');
this._data.hasIcon = false;
}
}
/**
* @method
* @name _updateIconSize
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since January 2024
* @description
* <blockquote>
* Update the icon size in output map
* </blockquote>
* @param {Event} e - The range input
**/
_updateIconSize(e) {
if (CONST.DEBUG) { console.log('MesseMap._updateIconSize() called with ', e); }
document.getElementById('map-icon').style.height = `${e.target.value}rem`;
document.getElementById('map-icon').style.width = `${e.target.value}rem`;
const label = e.target.previousElementSibling;
this._data.icon.size = parseInt(e.target.value);
// Update position with new size
e.target.dataset.size = e.target.value;
label.innerHTML = `${this._nls.icon.size} : ${e.target.value}`;
}
/**
* @method
* @name _handleIconDrag
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since January 2024
* @description
* <blockquote>
* Handle the icon dragging in the map output
* </blockquote>
**/
_handleIconDrag() {
if (CONST.DEBUG) { console.log('MesseMap._handleIconDrag() called'); }
let dragging = false;
const map = document.getElementById('map-output');
const mapRect = map.getBoundingClientRect();
const element = document.getElementById('map-icon');
let rect = element.getBoundingClientRect();
// Map paddings
let paddingLeft = parseInt(window.getComputedStyle(map).getPropertyValue('padding-left').slice(0, -2));
let paddingTop = parseInt(window.getComputedStyle(map).getPropertyValue('padding-top').slice(0, -2));
// User pointer position
let clickX = 0;
let clickY = 0;
// Variation between place clicked and icon X,Y absolute origin
let dX = 0;
let dY = 0;
// Current position
let positionX = 0;
let positionY = 0;
const mouseDown = e => {
e.preventDefault();
dragging = true;
// Update icon bounding rect in case its sized was updated
rect = element.getBoundingClientRect();
// Update map paddings if style has been updated
paddingLeft = parseInt(window.getComputedStyle(map).getPropertyValue('padding-left').slice(0, -2));
paddingTop = parseInt(window.getComputedStyle(map).getPropertyValue('padding-top').slice(0, -2));
// Store initial position
positionX = e.clientX;
positionY = e.clientY;
// Get dif between click and icon x pos
dX = e.clientX - rect.x;
dY = e.clientY - rect.y;
};
const mouseMove = e => {
const xAxis = (e.pageX > (mapRect.x + paddingLeft - dX) && e.pageX < (mapRect.x + mapRect.width - paddingLeft + dX));
const yAxis = (e.pageY > (mapRect.y + paddingTop - dY) && e.pageY < (mapRect.y + mapRect.height - paddingTop + dY));
if (dragging && xAxis && yAxis) {
e.preventDefault();
// Update pointer position with differential
clickX = positionX - e.clientX;
clickY = positionY - e.clientY;
// Store current pointer position
positionX = e.clientX;
positionY = e.clientY;
// Determine icon new top and left position
const newTop = element.offsetTop - clickY;
const newLeft = element.offsetLeft - clickX;
// Only update Y if in bounds
if (newTop > paddingTop && newTop + rect.width < mapRect.height - paddingTop) {
element.style.top = `${newTop}px`;
element.dataset.top = `${newTop}px`;
this._data.icon.y = newTop * 100 / mapRect.height;
}
// Only update X if in bounds
if (newLeft > paddingLeft && newLeft + rect.width < mapRect.width - paddingLeft) {
element.style.left = `${newLeft}px`;
element.dataset.left = `${newLeft}px`;
this._data.icon.x = newLeft * 100 / mapRect.width;
}
}
};
const mouseUp = () => {
dragging = false;
};
element.addEventListener('mousedown', mouseDown);
map.addEventListener('mousemove', mouseMove);
window.addEventListener('mouseup', mouseUp);
}
/**
* @method
* @name _updateIconImage
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since January 2024
* @description
* <blockquote>
* Update the icon source image
* </blockquote>
* @param {Event} e - The clicked image
**/
_updateIconImage(e) {
if (CONST.DEBUG) { console.log('MesseMap._updateIconImage() called with ', e); }
const icons = document.getElementById('icon-images');
for (let i = 0; i < icons.children.length; ++i) {
if (icons.children[i].classList.contains('selected')) {
icons.children[i].classList.remove('selected');
break;
}
}
// Update icon src and store it
e.target.classList.add('selected');
this._data.icon.image = e.target.dataset.url; // Update internal data
document.getElementById('map-icon').style.mask = `url(${e.target.dataset.url}) no-repeat center / contain`;
}
/**
* @method
* @name _updateIconColor
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since January 2024
* @description
* <blockquote>
* Update the icon color
* </blockquote>
* @param {Event} e - The color input
**/
_updateIconColor(e) {
document.getElementById('map-icon').style.backgroundColor = e.target.value;
document.getElementById('map-icon').style.mask = `url(${this._data.icon.image}) no-repeat center / contain`;
this._data.icon.color = e.target.value;
document.documentElement.style.setProperty('--fillColor', this._data.icon.color);
}
/**
* @method
* @name _prepareIconForPrinting
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since January 2024
* @description
* <blockquote>
* Apply svg image with color to properly render icon ou output map
* </blockquote>
* @param {Number} scale - The output map scale
**/
_prepareIconForPrinting(scale) {
return new Promise(resolve => {
fetch(this._data.icon.image)
.then(response => response.text())
.then((data) => {
var parser = new DOMParser();
var svg = parser.parseFromString(data, 'image/svg+xml').lastChild;
svg.style.width = '100%';
svg.style.height = '100%';
const wrapper = document.createElement('DIV');
wrapper.style.position = 'absolute';
wrapper.style.zIndex = '999';
wrapper.style.width = `${this._data.icon.size * scale}rem`;
wrapper.style.height = `${this._data.icon.size * scale}rem`;
wrapper.style.left = `calc(${this._data.icon.x}% - ${this._data.icon.size}px)`;
wrapper.style.top = `calc(${this._data.icon.y}% - ${this._data.icon.size}px`;
wrapper.appendChild(svg);
document.getElementById('map-output').appendChild(wrapper);
requestAnimationFrame(resolve);
}).catch(resolve);
});
}
/**
* @method
* @name _updateDimensionClicked
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since April 2023
* @description
* <blockquote>
* Update the export resolution slider label and buttons according to a click on a button
* </blockquote>
* @param {Event} e - The input event on the size span
**/
_updateDimensionClicked(e) {
let value = 600;
if (e.target.dataset.size === 'A2') {
value = 4962;
} else if (e.target.dataset.size === 'A3') {
value = 3509;
} else if (e.target.dataset.size === 'A4') {
value = 2481;
} else if (e.target.dataset.size === 'A5') {
value = 1755;
} else if (e.target.dataset.size === 'A6') {
value = 1242;
}
this._updateDimensionLabel({
target: {
value: value,
previousElementSibling: document.getElementById('image-width').previousElementSibling,
dataset: {
height: document.getElementById('image-width').dataset.height
}
}
});
document.getElementById('image-width').value = value;
}
/**
* @method
* @name _updateDimensionLabel
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Update the export resolution slider label to notify the user the size of the generated
* poster, at 300 DPI.
* </blockquote>
* @param {Event} e - The input event on the resolution slider
**/
_updateDimensionLabel(e) {
if (CONST.DEBUG) { console.log('MesseMap._updateDimensionLabel() called with ', e); }
const label = e.target.previousElementSibling;
let a = '7';
if (e.target.value > 4961) {
a = '2';
} else if (e.target.value > 3508) {
a = '3';
} else if (e.target.value > 2480) {
a = '4';
} else if (e.target.value > 1754) {
a = '5';
} else if (e.target.value > 1241) {
a = '6';
}
// Update label with slider value, computed height and matching paper format
const height = this.precisionRound(e.target.value * 29.7 / 21, 0);
e.target.dataset.height = height;
if (document.getElementById('map-output').classList.contains('horizontal')) {
label.innerHTML = `${this._nls.export.dimension} : ${height} x ${e.target.value} — A${a} ${this._nls.export.at} 300dpi`;
} else {
label.innerHTML = `${this._nls.export.dimension} : ${e.target.value} x ${height} — A${a} ${this._nls.export.at} 300dpi`;
}
// Update selection buttons
const sizes = document.getElementById('size-container');
for (let i = 0; i < sizes.children.length; ++i) {
if (sizes.children[i].dataset.size === `A${a}`) {
sizes.children[i].classList.add('selected');
} else {
sizes.children[i].classList.remove('selected');
}
}
}
/**
* @method
* @name _searchMatch
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since November 2022
* @description
* <blockquote>
* Set map view depending on search result
* </blockquote>
* @param {Object} data - The search data result to set view from
**/
_searchMatch(data) {
if (CONST.DEBUG) { console.log('MesseMap._searchMatch() called with ', data); }
this._map.setView(data.latlng, 11.5);
}
/**
* @method
* @name _updateLang
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since November 2022
* @description
* <blockquote>
* Updates the user interface language according to credit select value then reload page
* </blockquote>
* @param {Event} e - The input event on the select input
**/
_updateLang(e) {
if (CONST.DEBUG) { console.log('MesseMap._updateLang() called with ', e); }
localStorage.setItem('lang', e.target.value);
window.location.reload();
}
// ======================================================================= //
// ------------------ Printing and downloading methods ------------------- //
// ======================================================================= //
/**
* @method
* @name _download
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Start the download routine. First, hide the app with a loading overlay,
* then apply the user output dimension (set from the slider) to scale the
* poster to its final dimension, then ensure all tiles are loaded before
* starting html2canvas on that DIV. Finally, export to disk the canvas to
* image and restore the DOM to its initial size and scale.
* </blockquote>
**/
_download() {
if (CONST.DEBUG) { console.log('MesseMap._download() called'); }
this.blockInterface().then(() => {
// First we get the user desired size
let size = document.getElementById('image-width').value;
let scale = size / 600;
if (document.getElementById('map-output').classList.contains('horizontal')) {
const height = document.getElementById('image-width').dataset.height;
scale = height / 600;
size = height;
}
const bounds = this._map.getBounds(); // Map bound before scaling
// Scale map elements according to user desired size
this._dlPrepareMap(size, scale, bounds).then(() => {
// setInterval on mapPrint to ensure tiles are loaded before downloading (tilesLoaded flag)
if (scale === 1) { this._tilesLoaded = true; } // Set tiles loaded if no upscale is requested on export
// Set tiles loaded flag to false to wait for reframe to occur
this._intervalId = setInterval(this._dlPerformMapPrint.bind(this, bounds), 2000);
});
});
}
/**
* @method
* @name _dlPrepareMap
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Before starting the downloading of the poster, the map must be scaled in order to
* honor the requested dimension. To do so, the map-output div is scaled according to the
* slider position, and the leaflet map is properly positionned to keep the user's viewport,
* not matter the upscale it request (there is a limitation with maxZoom of the map, which
* can not be overpassed).
* </blockquote>
* @param {Number} width - The poster width to use
* @param {Number} scale - The poster scale compared to the classical 600px wide one
* @param {Object} bounds - The map bounds requested for the printing
**/
_dlPrepareMap(size, scale, bounds) {
if (CONST.DEBUG) { console.log('MesseMap._dlPrepareMap() : Prepare map style for printing'); }
return new Promise(resolve => {
document.getElementById('print-status').innerHTML = this._nls.download.stylePrep;
document.getElementById('print-progress').style.width = '10%';
// Hide Leaflet.js overlays
document.querySelector('.leaflet-top.leaflet-left').style.display = 'none';
document.querySelector('.leaflet-top.leaflet-right').style.display = 'none';
// Scale CSS variables
const cssVars = {
padding: parseInt(window.getComputedStyle(document.getElementById('map-output')).getPropertyValue('--padding').replace('rem', '')),
thickBorder: parseInt(window.getComputedStyle(document.getElementById('map-output')).getPropertyValue('--thick-border').replace('px', '')),
smallBorder: parseInt(window.getComputedStyle(document.getElementById('map-output')).getPropertyValue('--small-border').replace('px', ''))
};
document.getElementById('map-output').style.setProperty('--padding', `${cssVars.padding * scale}rem`);
document.getElementById('map-output').style.setProperty('--thick-border', `${cssVars.thickBorder * scale}px`);
document.getElementById('map-output').style.setProperty('--small-border', `${cssVars.smallBorder * scale}px`);
document.body.style.fontSize = `${1.2 * scale}rem`;
// Scale map dimension and attributes
if (document.getElementById('map-output').classList.contains('horizontal')) {
document.getElementById('map-output').style.height = `${size}px`;
} else {
document.getElementById('map-output').style.width = `${size}px`;
}
// Update icon position
if (this._data.hasIcon === true) {
document.getElementById('map-icon').style.display = 'none';
}
// Mobile specific
if (document.body.clientWidth < 1150) {
document.querySelector('.user-text-wrapper').style.fontSize = 'inherit';
}
document.getElementById('map-output').style.position = 'absolute';
// Remove box shadow from map container
document.getElementById('map-output').classList.remove('shadow');
document.getElementById('map-output').style.boxShadow = 'none';
requestAnimationFrame(() => {
this._map.invalidateSize();
this._map.fitBounds(bounds, { duration: 0 });
if (CONST.DEBUG) { console.log('MesseMap._dlPrepareMap() : Waiting for map tiles to load'); }
document.getElementById('print-status').innerHTML = this._nls.download.tileLoad;
document.getElementById('print-progress').style.width = '25%';
this._tilesLoaded = false;
// Need to create svg if an icon is requested
if (this._data.hasIcon === true) {
this._prepareIconForPrinting(scale).then(resolve);
} else {
resolve();
}
});
});
}
/**
* @method
* @name _dlPerformMapPrint
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* The method will perform its logic only if all tiles are loaded. If so, it will use html2canvas library
* to perform a conversion between poster into a canvas that will be later used to save the poster
* to the user disk. Call this method in a setInterval to regulary test if tiles are loaded.
* </blockquote>
* @param {Object} bounds - The map bounds requested for the printing (here just to be passe to _dlRestoreMap())
**/
_dlPerformMapPrint(bounds) {
// Perform map print with html2canvas if all tiles are loaded
if (this._tilesLoaded === true) {
if (CONST.DEBUG) { console.log('MesseMap._dlPerformMapPrint() : Map tiles loaded, performing printing'); }
document.getElementById('print-status').innerHTML = this._nls.download.printStart;
document.getElementById('print-progress').style.width = '66%';
clearInterval(this._intervalId);
requestAnimationFrame(() => {
// Execute html2canvas with output div
window.html2canvas(document.getElementById('map-output'), {
proxy: '/proxy',
logging: CONST.DEBUG,
width: document.getElementById('map-output').offsetWidth,
height: document.getElementById('map-output').offsetHeight,
imageTimeout: 0,
onclone: () => {
document.getElementById('print-status').innerHTML = this._nls.download.outputFile;
document.getElementById('print-progress').style.width = '72%';
}
}).then(this._dlMap.bind(this, bounds)).catch((error) => {
console.error(error);
this._dlRestoreMap(bounds);
});
});
}
}
/**
* @method
* @name _dlMap
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* The method will export the canvas previously drawn to the user disk, with the
* curently selected format in the aside. (using jsPDF if so, otherwise, classical href with dataUrl)
* </blockquote>
* @param {Object} bounds - The map bounds requested for the printing (here just to be passe to _dlRestoreMap())
* @param {Object} canvas - The canvas that holds the poster data to be exported to disk
**/
_dlMap(bounds, canvas) {
if (CONST.DEBUG) { console.log('MesseMap._dlMap() : Canvas printing done, exporting image to disk'); }
document.getElementById('print-status').innerHTML = this._nls.download.saveToDisk;
document.getElementById('print-progress').style.width = '88%';
const file = this.getOutputFileType();
const link = document.createElement('A');
link.download = `${document.getElementById('title').innerHTML}.${file.extension}`;
if (file.type === 'pdf') {
const pageFormat = document.getElementById('image-width-label').innerHTML.split('—')[1].replace(' ', '').substring(0, 2);
let pdf = new jsPDF({
orientation: (document.getElementById('map-output').classList.contains('horizontal')) ? 'landscape' : 'portrait',
format: pageFormat,
precision: 32
});
const width = pdf.internal.pageSize.getWidth();
const height = pdf.internal.pageSize.getHeight();
pdf.addImage(canvas.toDataURL('image/png', 1.0), 'PNG', 0, 0, width, height);
pdf.save(`${document.getElementById('title').innerHTML}.pdf`);
} else {
link.href = canvas.toDataURL(`image/${file.type}`, 1.0);
link.click();
}
// Send data parameters to server after user download
this._sendImageParameters();
// Restore map to default value
this._dlRestoreMap(bounds);
}
/**
* @method
* @name _dlRestoreMap
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* When the poster downloading is done, this method is called to cleanup the map
* and to restore it to its default size to be then used again.
* </blockquote>
* @param {Object} bounds - The map bounds requested for the printing to restore the map with proper viewport
**/
_dlRestoreMap(bounds) {
if (CONST.DEBUG) { console.log('MesseMap._dlRestoreMap() : Restoring map style to default'); }
document.getElementById('print-status').innerHTML = this._nls.download.restoreMap;
document.getElementById('print-progress').style.width = '100%';
// Restore Leaflet.js overlays
document.querySelector('.leaflet-top.leaflet-left').style.display = 'inherit';
document.querySelector('.leaflet-top.leaflet-right').style.display = 'inherit';
document.body.style.fontSize = `1.2rem`;
// Restore map inline styles and variables
if (document.body.clientWidth < 1150) {
document.querySelector('.user-text-wrapper').style.fontSize = '50%';
}
// Restore icon position
if (this._data.hasIcon === true) {
document.getElementById('map-icon').style.display = 'block';
document.getElementById('map-output').removeChild(document.getElementById('map-output').lastElementChild);
}
// Clear map temporary styles and remove temporary
document.getElementById('map-output').style = '';
// Restore map container box shadow
document.getElementById('map-output').classList.add('shadow');
// Remove print overlay
setTimeout(() => {
this._map.invalidateSize();
this._map.fitBounds(bounds, { duration: 0 });
this.unblockInterface().then(() => {
if (CONST.DEBUG) { console.log('MesseMap._dlRestoreMap() : Map properly restored'); }
document.getElementById('print-progress').style.width = '0';
});
}, 200);
}
/**
* @method
* @name _sendImageParameters
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since April 2023
* @description
* <blockquote>
* Perform a POST call to the server to save map parameters as JSON
* </blockquote>
**/
_sendImageParameters() {
// Discrete saving, no then/catch as error is handled in backend log.
fetch('/upload', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(this.formatImageParameters()),
});
}
// ======================================================================= //
// --------------------------- Modal methods ----------------------------- //
// ======================================================================= //
/**
* @method
* @name _themeEditModal
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* This method will build th theme edit modal into the user interface.
* </blockquote>
* @param {Event} e - The click event that triggered the modal
**/
_themeEditModal(e) {
if (CONST.DEBUG) { console.log('MesseMap._themeEditModal() called with ', e); }
e.preventDefault();
this._fetchModal('themeedit').then(dom => {
const _updateInputs = () => {
document.getElementById('light-bg-color').value = this._cssTheme.lbg;
document.getElementById('light-txt-color').value = this._cssTheme.ltxt;
document.getElementById('light-txt-alt-color').value = this._cssTheme.lcom;
document.getElementById('dark-bg-color').value = this._cssTheme.dbg;
document.getElementById('dark-txt-color').value = this._cssTheme.dtxt;
document.getElementById('dark-txt-alt-color').value = this._cssTheme.dcom;
};
const _updateColor = e => {
document.querySelector(':root').style.setProperty(`--color-${e.target.dataset.key}`, e.target.value);
this.updateThemeColorInternal();
};
// Apply current theme
this.updateThemeColorInternal();
// Modal start animation (close animation handled in _closeModal())
document.getElementById('modal-overlay').appendChild(dom);
document.getElementById('modal-overlay').style.display = 'flex';
setTimeout(() => document.getElementById('modal-overlay').style.opacity = 1, 50);
requestAnimationFrame(() => {
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_TITLE}}', this._nls.theme.title);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_LIGHT_THEME}}', this._nls.theme.lightTheme);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_DARK_THEME}}', this._nls.theme.darkTheme);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_BG_LIGHT}}', this._nls.theme.bg);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_TXT_LIGHT}}', this._nls.theme.txt);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_TXTALT_LIGHT}}', this._nls.theme.txtAlt);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_BG_DARK}}', this._nls.theme.bg);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_TXT_DARK}}', this._nls.theme.txt);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_TXTALT_DARK}}', this._nls.theme.txtAlt);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_RESET}}', this._nls.theme.default);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_CLOSE}}', this._nls.action.close);
// Need to postpone events as their text changed, and won't trigger previously subscribed event
requestAnimationFrame(() => {
// Color input Listeners
document.getElementById('light-bg-color').addEventListener('input', _updateColor);
document.getElementById('light-txt-color').addEventListener('input', _updateColor);
document.getElementById('light-txt-alt-color').addEventListener('input', _updateColor);
document.getElementById('dark-bg-color').addEventListener('input', _updateColor);
document.getElementById('dark-txt-color').addEventListener('input', _updateColor);
document.getElementById('dark-txt-alt-color').addEventListener('input', _updateColor);
// Close modal button event
document.getElementById('close').addEventListener('click', this._closeModal.bind(this, null, true));
document.getElementById('reset').addEventListener('click', () => {
this._cssTheme.lbg = '#FFFFFE';
this._cssTheme.ltxt = '#000001';
this._cssTheme.lcom = '#999998';
this._cssTheme.dbg = '#000001';
this._cssTheme.dtxt = '#FFFFFE';
this._cssTheme.dcom = '#999998';
this.applyThemeColor();
_updateInputs();
});
});
_updateInputs();
});
});
}
/**
* @method
* @name _creditModal
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* This method will build the credit modal into the user interface.
* </blockquote>
* @param {Event} e - The click event that triggered the modal
**/
_creditModal(e) {
if (CONST.DEBUG) { console.log('MesseMap._creditModal() called with ', e); }
e.preventDefault();
this._fetchModal('credits').then(dom => {
// Modal start animation (close animation handled in _closeModal())
document.getElementById('modal-overlay').appendChild(dom);
document.getElementById('modal-overlay').style.display = 'flex';
setTimeout(() => document.getElementById('modal-overlay').style.opacity = 1, 50);
requestAnimationFrame(() => {
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_TITLE}}', `${this._nls.title} – ${CONST.VERSION}`);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_LINE1}}', this._nls.credit.line1);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_LINE2}}', this._nls.credit.line2);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_LINE3}}', this._nls.credit.line3);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_LINE4}}', this._nls.credit.line4);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_LINE5}}', this._nls.credit.line5);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_LANG}}', this._nls.credit.lang);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_FR}}', this._nls.credit.fr);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_EN}}', this._nls.credit.en);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_ES}}', this._nls.credit.es);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_DE}}', this._nls.credit.de);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_IT}}', this._nls.credit.it);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_PT}}', this._nls.credit.pt);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_PL}}', this._nls.credit.pl);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_RU}}', this._nls.credit.ru);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_CH}}', this._nls.credit.ch);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_JA}}', this._nls.credit.ja);
this.replaceString(document.getElementById('modal-overlay'), '{{MODAL_CLOSE}}', this._nls.action.close);
// Lang update
document.getElementById('lang').value = this._lang;
document.getElementById('lang').addEventListener('change', this._updateLang.bind(this));
// Close modal button event
document.getElementById('close').addEventListener('click', this._closeModal.bind(this, null, true));
});
});
}
/**
* @method
* @name _fetchModal
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* This method will use the fetch API to request the modal HTMl file
* stored in project <code>assets/html</code>.
* </blockquote>
* @param {String} url - The modal filename with no extension in /assets/html/
* @returns {Promise} A resolved or rejected Promise
**/
_fetchModal(url) {
if (CONST.DEBUG) { console.log('MesseMap._fetchModal() called with ', url); }
return new Promise(resolve => {
fetch(`assets/html/${url}.html`).then(data => {
data.text().then(html => {
resolve(document.createRange().createContextualFragment(html));
});
});
});
}
/**
* @method
* @name _closeModal
* @private
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Click on overlay callback event to test if modal needs to be closed. It can be
* bypassed with a flag to close modal no matter the context.
* </blockquote>
* @param {Event} event - The click event
* @param {Boolean} force - Pass it to true to close the modal no matter the context
**/
_closeModal(event, force) {
if (CONST.DEBUG) { console.log('MesseMap._closeModal() called with ', event, 'force : ', force); }
if (force === true || event.target.id === 'modal-overlay' || event.target.id.indexOf('close') !== -1) {
document.getElementById('modal-overlay').style.opacity = 0;
setTimeout(() => {
document.getElementById('modal-overlay').style.display = 'none';
document.getElementById('modal-overlay').innerHTML = '';
}, 300);
}
}
// ======================================================================= //
// ----------------------- Generic utils methods ------------------------- //
// ======================================================================= //
/**
* @method
* @name getOutputFileType
* @public
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Read DOM fieldset for output format, and return as Object the extension and type to be
* used when calling <code>toDataURL()</code> on output canvas with proper parameters.
* </blockquote>
* @returns {Object} An object that contains extension and type string for selected output type
**/
getOutputFileType() {
if (CONST.DEBUG) { console.log('MesseMap.getOutputFileType() called'); }
const file = {
extension: 'png',
type: 'png'
};
Array.from(document.getElementById('image-type').elements).forEach(el => {
if (el.checked === true) {
file.extension = el.dataset.extension;
file.type = el.dataset.type;
}
});
return file;
}
/**
* @method
* @name precisionRound
* @public
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Do a Math.round with a given precision (ie amount of integers after the coma).
* </blockquote>
* @param {Nunmber} value - The value to precisely round
* @param {Number} precision - The number of integers after the coma
* @return {Number} - The rounded value
**/
precisionRound(value, precision) {
if (CONST.DEBUG) { console.log('MesseMap.precisionRound() called with ', value, 'precision : ', precision); }
const multiplier = Math.pow(10, precision || 0);
return Math.round(value * multiplier) / multiplier;
}
/**
* @method
* @name updateThemeColorInternal
* @public
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Get css color and saves them to local storage
* </blockquote>
**/
updateThemeColorInternal() {
if (CONST.DEBUG) { console.log('MesseMap.updateThemeColorInternal() called'); }
// Update input.color values
this._cssTheme = {
lbg: window.getComputedStyle(document.querySelector(':root')).getPropertyValue('--color-l-bg'),
ltxt: window.getComputedStyle(document.querySelector(':root')).getPropertyValue('--color-l-txt'),
lcom: window.getComputedStyle(document.querySelector(':root')).getPropertyValue('--color-l-txt-alt'),
dbg: window.getComputedStyle(document.querySelector(':root')).getPropertyValue('--color-d-bg'),
dtxt: window.getComputedStyle(document.querySelector(':root')).getPropertyValue('--color-d-txt'),
dcom: window.getComputedStyle(document.querySelector(':root')).getPropertyValue('--color-d-txt-alt'),
};
localStorage.setItem('theme', JSON.stringify(this._cssTheme));
}
/**
* @method
* @name applyThemeColor
* @public
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Apply the css variables values to the root style from internal _cssTheme object
* </blockquote>
**/
applyThemeColor() {
if (CONST.DEBUG) { console.log('MesseMap.applyThemeColor() called'); }
document.querySelector(':root').style.setProperty('--color-l-bg', this._cssTheme.lbg);
document.querySelector(':root').style.setProperty('--color-l-txt', this._cssTheme.ltxt);
document.querySelector(':root').style.setProperty('--color-l-txt-alt', this._cssTheme.lcom);
document.querySelector(':root').style.setProperty('--color-d-bg', this._cssTheme.dbg);
document.querySelector(':root').style.setProperty('--color-d-txt', this._cssTheme.dtxt);
document.querySelector(':root').style.setProperty('--color-d-txt-alt', this._cssTheme.dcom);
this.updateThemeColorInternal();
}
/**
* @method
* @name replaceString
* @public
* @memberof MesseMap
* @author Arthur Beaulieu
* @since October 2022
* @description
* <blockquote>
* Will replace the element text. Useful for translations.
* </blockquote>
* @param {Nunmber} element - The DOM element for text to be replaced
* @param {Number} string - The string to replace
* @param {Number} value - The value to apply to the replaced text
**/
replaceString(element, string, value) {
if (CONST.DEBUG) { console.log('MesseMap.replaceString() called with ', element, 'string : ', string, 'value : ', value); }
element.innerHTML = element.innerHTML.replace(string, value);
}
/**
* @method
* @name formatImageParameters
* @public
* @memberof MesseMap
* @author Arthur Beaulieu
* @since April 2023
* @description
* <blockquote>
* Will format all map parameters into a JSON object ready to send
* </blockquote>
* @return {Object} - The map parameters
**/
formatImageParameters() {
return {
style: {
orientation: this._data.orientation,
style: this._data.style,
darkTheme: this._data.darkTheme,
upText: this._data.upText,
colors: this._cssTheme,
},
text: {
title: document.getElementById('user-title').value,
subtitle: document.getElementById('user-subtitle').value,
comment: document.getElementById('user-comment').value
},
icon: {
displayed: this._data.hasIcon,
color: this._data.icon.color,
image: this._data.icon.image,
size: this._data.icon.size,
x: this._data.icon.x,
y: this._data.icon.y
},
map: {
layer: this._data.layer,
center: this._map.getCenter(),
zoom: this._map.getZoom()
},
export: {
width: document.getElementById('image-width').value,
height: document.getElementById('image-width').dataset.height,
filtetype: this.getOutputFileType()
}
};
}
/**
* @method
* @name blockInterface
* @public
* @memberof MesseMap
* @author Arthur Beaulieu
* @since April 2023
* @description
* <blockquote>
* Will add a modal over the whole app to hide the page content
* </blockquote>
* @return {Promise} - A promise resolved when UI is blocked
**/
blockInterface() {
if (CONST.DEBUG) { console.log('MesseMap.blockInterface called'); }
return new Promise(resolve => {
document.getElementById('print-overlay').style.zIndex = 99;
document.getElementById('print-overlay').style.opacity = 1;
document.getElementById('map-output').style.transition = 'none';
setTimeout(resolve, 200);
});
}
/**
* @method
* @name unblockInterface
* @public
* @memberof MesseMap
* @author Arthur Beaulieu
* @since April 2023
* @description
* <blockquote>
* Will remove the modal over the whole app
* </blockquote>
* @return {Promise} - A promise resolved when UI is unblocked
**/
unblockInterface() {
if (CONST.DEBUG) { console.log('MesseMap.unblockInterface called'); }
return new Promise(resolve => {
document.getElementById('print-overlay').style.opacity = 0;
setTimeout(() => {
document.getElementById('print-overlay').style.zIndex = -1;
document.getElementById('print-overlay').children[0].innerHTML = this._nls.download.title;
document.getElementById('print-overlay').children[1].innerHTML = this._nls.download.subtitle;
resolve();
}, 200);
});
}
}
export default MesseMap;