UNPKG

kepler.gl

Version:

kepler.gl is a webgl based application to visualize large scale location data in the browser

285 lines (250 loc) 7.49 kB
// Copyright (c) 2020 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import moment from 'moment'; import assert from 'assert'; import {ALL_FIELD_TYPES} from 'constants/default-settings'; const MAX_LATITUDE = 90; const MIN_LATITUDE = -90; const MAX_LONGITUDE = 180; const MIN_LONGITUDE = -180; /** * simple getting unique values of an array * * @param {array} values * @returns {array} unique values */ export function unique(values) { const results = []; values.forEach(v => { if (!results.includes(v) && notNullorUndefined(v)) { results.push(v); } }); return results; } /* eslint-disable max-statements */ /** * return center of map from given points * @param {array} layers * @param {string} dataId * @returns {object} coordinates of map center, empty if not found */ export function findMapBounds(layers) { // find bounds in formatted layerData // take ALL layers into account when finding map bounds const availableLayerBounds = layers.reduce((res, l) => { if (l.meta && l.meta.bounds) { res.push(l.meta.bounds); } return res; }, []); // return null if no layer is available if (availableLayerBounds.length === 0) { return null; } // merge bounds in each layer const newBounds = availableLayerBounds.reduce( (res, b) => { return [ Math.min(res[0], b[0]), Math.min(res[1], b[1]), Math.max(res[2], b[2]), Math.max(res[3], b[3]) ]; }, [MAX_LONGITUDE, MAX_LATITUDE, MIN_LONGITUDE, MIN_LATITUDE] ); return newBounds; } /* eslint-enable max-statements */ export function getLatLngBounds(points, idx, limit) { const lats = points .map(d => Array.isArray(d) && d[idx]) .filter(Number.isFinite) .sort(numberSort); if (!lats.length) { return null; } // clamp to limit return [Math.max(lats[0], limit[0]), Math.min(lats[lats.length - 1], limit[1])]; } export function clamp([min, max], val) { return val <= min ? min : val >= max ? max : val; } export function getSampleData(data, sampleSize = 500, getValue = d => d) { const sampleStep = Math.max(Math.floor(data.length / sampleSize), 1); const output = []; for (let i = 0; i < data.length; i += sampleStep) { output.push(getValue(data[i])); } return output; } /** * Convert different time format to unix milliseconds * @param {*} value * @param {*} format */ export function timeToUnixMilli(value, format) { if (notNullorUndefined(value)) { return typeof value === 'string' ? moment.utc(value, format).valueOf() : format === 'x' ? value * 1000 : value; } return null; } export function maybeToDate(isTime, fieldIdx, format, d) { if (isTime) { return timeToUnixMilli(d[fieldIdx], format); } return d[fieldIdx]; } /** * whether null or undefined * @returns {boolean} - yes or no */ export function notNullorUndefined(d) { return d !== undefined && d !== null; } export function isPlainObject(obj) { return obj === Object(obj) && typeof obj !== 'function' && !Array.isArray(obj); } export function numberSort(a, b) { return a - b; } export function getSortingFunction(fieldType) { switch (fieldType) { case ALL_FIELD_TYPES.real: case ALL_FIELD_TYPES.integer: case ALL_FIELD_TYPES.timestamp: return numberSort; default: return undefined; } } /** * round number with exact number of decimals * return as a string * @param {number} num * @param {number} decimals * @returns {string} - a rounded number in string format */ export function preciseRound(num, decimals) { const t = Math.pow(10, decimals); return ( Math.round( num * t + (decimals > 0 ? 1 : 0) * (Math.sign(num) * (10 / Math.pow(100, decimals))) ) / t ).toFixed(decimals); } /** * get number of decimals to round to for slider from step * @param {number} step * @returns {number} - number of decimal */ export function getRoundingDecimalFromStep(step) { if (isNaN(step)) { assert('step is not a number'); assert(step); } const splitZero = step.toString().split('.'); if (splitZero.length === 1) { return 0; } return splitZero[1].length; } /** * round the value to step for the slider * @param {number} minValue * @param {number} step * @param {number} val * @returns {number} - rounded number */ export function roundValToStep(minValue, step, val) { if (isNaN(step)) { return val; } const decimal = getRoundingDecimalFromStep(step); const steps = Math.floor((val - minValue) / step); let remain = val - (steps * step + minValue); // has to round because javascript turns 0.1 into 0.9999999999999987 remain = Number(preciseRound(remain, 8)); let closest; if (remain === 0) { closest = val; } else if (remain < step / 2) { closest = steps * step + minValue; } else { closest = (steps + 1) * step + minValue; } // precise round return a string rounded to the defined decimal const rounded = preciseRound(closest, decimal); return Number(rounded); } const identity = d => d; export const FIELD_DISPLAY_FORMAT = { [ALL_FIELD_TYPES.string]: identity, [ALL_FIELD_TYPES.timestamp]: identity, [ALL_FIELD_TYPES.integer]: identity, [ALL_FIELD_TYPES.real]: identity, [ALL_FIELD_TYPES.boolean]: d => String(d), [ALL_FIELD_TYPES.date]: identity, [ALL_FIELD_TYPES.geojson]: d => typeof d === 'string' ? d : isPlainObject(d) ? JSON.stringify(d) : Array.isArray(d) ? `[${String(d)}]` : '' }; /** * Parse field value and type and return a string representation * @param {string} value the field value * @param {string} type the field type * @return {*} */ export const parseFieldValue = (value, type) => { if (!notNullorUndefined(value)) { return ''; } return FIELD_DISPLAY_FORMAT[type] ? FIELD_DISPLAY_FORMAT[type](value) : String(value); }; const arrayMoveMutate = (array, from, to) => { array.splice(to < 0 ? array.length + to : to, 0, array.splice(from, 1)[0]); }; export const arrayMove = (array, from, to) => { array = array.slice(); arrayMoveMutate(array, from, to); return array; }; export function findFirstNoneEmpty(data, count = 1, getValue = identity) { let c = 0; const found = []; while (c < count && c < data.length) { const value = getValue(data[c]); if (notNullorUndefined(value)) { found.push(value); } c++; } return found; }