pixelplanet/src/core/utils.js

415 lines
9.7 KiB
JavaScript

import {
TILE_SIZE,
THREE_TILE_SIZE,
TILE_ZOOM_LEVEL,
} from './constants';
/**
* http://stackoverflow.com/questions/4467539/javascript-modulo-not-behaving
* @param n
* @param m
* @returns {number} remainder
*/
export function mod(n, m) {
return ((n % m) + m) % m;
}
/*
* returns random integer
* @param min Minimum of random integer
* @param max Maximum of random integer
* @return random integer between min and max (min <= ret <= max)
*/
export function getRandomInt(min, max) {
const range = max - min + 1;
return min + (Math.floor(Math.random() * range));
}
/*
* generates random string with a-z,0-9
* 11 chars length
*/
export function getRandomString() {
return Math.random().toString(36).substring(2, 15);
}
export function distMax([x1, y1], [x2, y2]) {
return Math.max(Math.abs(x1 - x2), Math.abs(y1 - y2));
}
export function clamp(n, min, max) {
return Math.max(min, Math.min(n, max));
}
/*
* convert YYYY-MM-DD to YYYYMMDD
*/
export function dateToString(date) {
// YYYY-MM-DD
return date.substring(0, 4) + date.substring(5, 7) + date.substring(8);
}
/*
* get current date in YYYY-MM-DD
*/
export function getToday() {
const date = new Date();
let day = date.getDate();
let month = date.getMonth() + 1;
if (month < 10) month = `0${month}`;
if (day < 10) day = `0${day}`;
return `${date.getFullYear()}-${month}-${day}`;
}
// z is assumed to be height here
// in ui and rendeer, y is height
export function getChunkOfPixel(
canvasSize,
x,
y,
z = null,
) {
const tileSize = (z === null) ? TILE_SIZE : THREE_TILE_SIZE;
const width = (z == null) ? y : z;
const cx = Math.floor((x + (canvasSize / 2)) / tileSize);
const cy = Math.floor((width + (canvasSize / 2)) / tileSize);
return [cx, cy];
}
// get coordinates of top-left corner of chunk
export function getCornerOfChunk(
canvasSize,
i,
j,
is3d = false,
) {
const tileSize = (is3d) ? THREE_TILE_SIZE : TILE_SIZE;
const x = (i * tileSize) - (canvasSize / 2);
const y = (j * tileSize) - (canvasSize / 2);
return [x, y, 0];
}
export function getTileOfPixel(
tileScale,
pixel,
canvasSize = null,
) {
const target = pixel.map(
(x) => Math.floor((x + canvasSize / 2) / TILE_SIZE * tileScale),
);
return target;
}
export function getMaxTiledZoom(canvasSize) {
if (!canvasSize) return 0;
return Math.log2(canvasSize / TILE_SIZE) / TILE_ZOOM_LEVEL * 2;
}
export function getHistoricalCanvasSize(
historicalDate,
canvasSize,
historicalSizes,
) {
if (historicalDate && historicalSizes) {
let i = historicalSizes.length;
while (i > 0) {
i -= 1;
const [date, size] = historicalSizes[i];
if (historicalDate <= date) {
return size;
}
}
}
return canvasSize;
}
export function getCanvasBoundaries(canvasSize) {
const canvasMinXY = -canvasSize / 2;
const canvasMaxXY = canvasSize / 2 - 1;
return [canvasMinXY, canvasMaxXY];
}
// z is assumed to be height here
// in ui and rendeer, y is height
export function getOffsetOfPixel(
canvasSize,
x,
y,
z = null,
) {
const tileSize = (z === null) ? TILE_SIZE : THREE_TILE_SIZE;
const width = (z == null) ? y : z;
let offset = (z === null) ? 0 : (y * tileSize * tileSize);
const modOffset = mod((canvasSize / 2), tileSize);
const cx = mod(x + modOffset, tileSize);
const cy = mod(width + modOffset, tileSize);
offset += (cy * tileSize) + cx;
return offset;
}
/*
* Searches Object for element with ident string and returns its key
* Used for getting canvas id from given ident-string (see canvases.json)
* @param obj Object
* @param ident ident string
* @return key
*/
export function getIdFromObject(obj, ident) {
const ids = Object.keys(obj);
for (let i = 0; i < ids.length; i += 1) {
const key = ids[i];
if (obj[key].ident === ident) {
return parseInt(key, 10);
}
}
return null;
}
// z is returned as height here
// in ui and rendeer, y is height
export function getPixelFromChunkOffset(
i,
j,
offset,
canvasSize,
is3d: boolean = false,
) {
const tileSize = (is3d) ? THREE_TILE_SIZE : TILE_SIZE;
const cx = offset % tileSize;
const off = offset - cx;
let cy = off % (tileSize * tileSize);
const z = (is3d) ? (off - cy) / tileSize / tileSize : null;
cy /= tileSize;
const devOffset = canvasSize / 2 / tileSize;
const x = ((i - devOffset) * tileSize) + cx;
const y = ((j - devOffset) * tileSize) + cy;
return [x, y, z];
}
export function getCellInsideChunk(
canvasSize,
pixel,
) {
return pixel.map((x) => mod(x + canvasSize / 2, TILE_SIZE));
}
export function screenToWorld(
state,
$viewport,
[x, y],
) {
const { view, viewscale } = state.canvas;
const [viewX, viewY] = view;
const { width, height } = $viewport;
return [
Math.floor(((x - (width / 2)) / viewscale) + viewX),
Math.floor(((y - (height / 2)) / viewscale) + viewY),
];
}
export function worldToScreen(
state,
$viewport,
[x, y],
) {
const { view, viewscale } = state.canvas;
const [viewX, viewY] = view;
const { width, height } = $viewport;
return [
((x - viewX) * viewscale) + (width / 2),
((y - viewY) * viewscale) + (height / 2),
];
}
export function durationToString(
ms,
smallest: boolean = false,
) {
const seconds = Math.ceil(ms / 1000);
let timestring;
if (seconds < 60 && smallest) {
timestring = seconds;
} else {
// eslint-disable-next-line max-len
timestring = `${Math.floor(seconds / 60)}:${(`0${seconds % 60}`).slice(-2)}`;
}
return timestring;
}
const postfix = ['k', 'M', 'B'];
export function numberToString(num) {
if (!num) {
return 'N/A';
}
if (num < 1000) {
return num;
}
let postfixNum = 0;
while (postfixNum < postfix.length) {
if (num < 10000) {
// eslint-disable-next-line max-len
return `${Math.floor(num / 1000)}.${(`0${Math.floor((num % 1000) / 10)}`).slice(-2)}${postfix[postfixNum]}`;
} if (num < 100000) {
// eslint-disable-next-line max-len
return `${Math.floor(num / 1000)}.${Math.floor((num % 1000) / 100)}${postfix[postfixNum]}`;
} if (num < 1000000) {
return Math.floor(num / 1000) + postfix[postfixNum];
}
postfixNum += 1;
num = Math.round(num / 1000);
}
return '';
}
export function numberToStringFull(num) {
if (num < 0) {
return `${num} :-(`;
} if (num < 1000) {
return num;
} if (num < 1000000) {
return `${Math.floor(num / 1000)}.${(`00${String(num % 1000)}`).slice(-3)}`;
}
// eslint-disable-next-line max-len
return `${Math.floor(num / 1000000)}.${(`00${String(Math.floor(num / 1000))}`).slice(-3)}.${(`00${String(num % 1000)}`).slice(-3)}`;
}
/*
* generates a color based on a given string
*/
export function colorFromText(str) {
if (!str) return '#000000';
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const c = (hash & 0x00FFFFFF)
.toString(16)
.toUpperCase();
return `#${'00000'.substring(0, 6 - c.length)}${c}`;
}
/*
* sets a color into bright or dark mode
*/
export function setBrightness(hex, dark: boolean = false) {
hex = hex.replace(/^\s*#|\s*$/g, '');
if (hex.length === 3) {
hex = hex.replace(/(.)/g, '$1$1');
}
let r = Math.floor(parseInt(hex.substring(0, 2), 16) / 2);
let g = Math.floor(parseInt(hex.substring(2, 4), 16) / 2);
let b = Math.floor(parseInt(hex.substring(4, 6), 16) / 2);
if (dark) {
r += 128;
g += 128;
b += 128;
}
return `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`;
}
/*
* escape string for use in regexp
* @param string input string
* @return escaped string
*/
export function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/*
* check if webGL2 is available
* @return boolean true if available
*/
export function isWebGL2Available() {
try {
const canvas = document.createElement('canvas');
return !!(window.WebGL2RenderingContext && canvas.getContext('webgl2'));
} catch {
return false;
}
}
/*
* gets a descriptive text of the domain of the link
* Example:
* https://www.youtube.com/watch?v=G8APgeFfkAk returns 'youtube'
* http://www.google.at returns 'google.at'
* (www. and .com are split)
*/
export function getLinkDesc(link) {
let domainStart = link.indexOf('://') + 3;
if (domainStart < 3) {
domainStart = 0;
}
if (link.startsWith('www.', domainStart)) {
domainStart += 4;
}
let domainEnd = link.indexOf('/', domainStart);
if (domainEnd === -1) {
domainEnd = link.length;
}
if (link.endsWith('.com', domainEnd)) {
domainEnd -= 4;
}
if (domainEnd <= domainStart) {
return link;
}
return link.slice(domainStart, domainEnd);
}
/*
* try to get extension out of link
* @param link url
* @return extension or null if not available
*/
export function getExt(link) {
let paramStart = link.indexOf('&');
if (paramStart === -1) {
paramStart = link.length;
}
let posDot = paramStart - 1;
for (;posDot >= 0 && link[posDot] !== '.'; posDot -= 1) {
if (link[posDot] === '/') {
return null;
}
}
posDot += 1;
if (paramStart - posDot > 4) {
return null;
}
return link.slice(posDot, paramStart);
}
/*
* Split query part from link
* @param link url
* @return link without query
*/
export function stripQuery(link) {
let posAnd = link.indexOf('?');
if (posAnd === -1) posAnd = link.indexOf('#');
return (posAnd === -1) ? link : link.substring(0, posAnd);
}
/*
* convert timestamp to human readable date/time string
* @param timestamp Unix timestamp in seconds
* @return descriptive string of time
*/
export function getDateTimeString(timestamp) {
const curDate = new Date();
const date = new Date(timestamp * 1000);
if (date.getDate() !== curDate.getDate()) {
return date.toLocaleString();
}
return date.toLocaleTimeString();
}