UNPKG

kepler.gl

Version:

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

415 lines (372 loc) 11.2 kB
// Copyright (c) 2021 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 assert from 'assert'; import {ALL_FIELD_TYPES} from 'constants/default-settings'; import {TOOLTIP_FORMATS, TOOLTIP_FORMAT_TYPES, TOOLTIP_KEY} from 'constants/tooltip'; import {format as d3Format} from 'd3-format'; import {bisectLeft} from 'd3-array'; import moment from 'moment-timezone'; 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 = []; const uniqueSet = new Set(values); uniqueSet.forEach(v => { if (notNullorUndefined(v)) { results.push(v); } }); return results; } /* eslint-disable max-statements */ /** * return center of map from given points * @param {array} layers * @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 * @type {typeof import('./data-utils').timeToUnixMilli} */ export function timeToUnixMilli(value, format) { if (notNullorUndefined(value)) { return typeof value === 'string' ? moment.utc(value, format).valueOf() : format === 'x' ? value * 1000 : value; } return null; } /** * * @type {typeof import('./data-utils').maybeToDate} */ export function maybeToDate(isTime, fieldIdx, format, dc, d) { if (isTime) { return timeToUnixMilli(dc.valueAt(d.index, fieldIdx), format); } return dc.valueAt(d.index, fieldIdx); } /** * whether null or undefined * @type {typeof import('./data-utils').notNullorUndefined} */ export function notNullorUndefined(d) { return d !== undefined && d !== null; } /** * Whether d is a number, this filtered out NaN as well * @type {typeof import('./data-utils').notNullorUndefined} */ export function isNumber(d) { return Number.isFinite(d); } /** * whether null or undefined */ export function isPlainObject(obj) { return obj === Object(obj) && typeof obj !== 'function' && !Array.isArray(obj); } /** * @type {typeof import('./data-utils').numberSort} */ export function numberSort(a, b) { return a - b; } /** * @type {typeof import('./data-utils').getSortingFunction} */ 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 * @type {typeof import('./data-utils').preciseRound} */ 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; } /** * Use in slider, given a number and an array of numbers, return the nears number from the array * @type {typeof import('./data-utils').snapToMarks} * @param value * @param marks */ export function snapToMarks(value, marks) { // always use bin x0 const i = bisectLeft(marks, value); if (i === 0) { return marks[i]; } else if (i === marks.length) { return marks[i - 1]; } const idx = marks[i] - value < value - marks[i - 1] ? i : i - 1; return marks[idx]; } /** * If marks is provided, snap to marks, if not normalize to step * @type {typeof import('./data-utils').normalizeSliderValue} * @param val * @param minValue * @param step * @param marks */ export function normalizeSliderValue(val, minValue, step, marks) { if (marks && marks.length) { return snapToMarks(val, marks); } return roundValToStep(minValue, step, val); } /** * round the value to step for the slider * @type {typeof import('./data-utils').roundValToStep} * @param minValue * @param step * @param val * @returns - rounded number */ export function roundValToStep(minValue, step, val) { if (!isNumber(step) || !isNumber(minValue)) { 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); } /** * Get the value format based on field and format options * Used in render tooltip value * @type {typeof import('./data-utils').defaultFormatter} */ export const defaultFormatter = v => (notNullorUndefined(v) ? String(v) : ''); export const FIELD_DISPLAY_FORMAT = { [ALL_FIELD_TYPES.string]: defaultFormatter, [ALL_FIELD_TYPES.timestamp]: defaultFormatter, [ALL_FIELD_TYPES.integer]: defaultFormatter, [ALL_FIELD_TYPES.real]: defaultFormatter, [ALL_FIELD_TYPES.boolean]: defaultFormatter, [ALL_FIELD_TYPES.date]: defaultFormatter, [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 * @type {typeof import('./data-utils').parseFieldValue} */ 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]); }; /** * * @param {*} array * @param {*} from * @param {*} to */ export const arrayMove = (array, from, to) => { array = array.slice(); arrayMoveMutate(array, from, to); return array; }; /** * Get the value format based on field and format options * Used in render tooltip value * @type {typeof import('./data-utils').getFormatter} * @param format * @param field */ export function getFormatter(format, field) { if (!format) { return defaultFormatter; } const tooltipFormat = Object.values(TOOLTIP_FORMATS).find(f => f[TOOLTIP_KEY] === format); if (tooltipFormat) { return applyDefaultFormat(tooltipFormat); } else if (typeof format === 'string' && field) { return applyCustomFormat(format, field); } return defaultFormatter; } export function applyDefaultFormat(tooltipFormat) { if (!tooltipFormat || !tooltipFormat.format) { return defaultFormatter; } switch (tooltipFormat.type) { case TOOLTIP_FORMAT_TYPES.DECIMAL: return d3Format(tooltipFormat.format); case TOOLTIP_FORMAT_TYPES.DATE: case TOOLTIP_FORMAT_TYPES.DATE_TIME: return datetimeFormatter(null)(tooltipFormat.format); case TOOLTIP_FORMAT_TYPES.PERCENTAGE: return v => `${d3Format(TOOLTIP_FORMATS.DECIMAL_DECIMAL_FIXED_2.format)(v)}%`; case TOOLTIP_FORMAT_TYPES.BOOLEAN: return getBooleanFormatter(tooltipFormat.format); default: return defaultFormatter; } } export function getBooleanFormatter(format) { switch (format) { case '01': return v => (v ? '1' : '0'); case 'yn': return v => (v ? 'yes' : 'no'); default: return defaultFormatter; } } // Allow user to specify custom tooltip format via config export function applyCustomFormat(format, field) { switch (field.type) { case ALL_FIELD_TYPES.real: case ALL_FIELD_TYPES.integer: return d3Format(format); case ALL_FIELD_TYPES.date: case ALL_FIELD_TYPES.timestamp: return datetimeFormatter(null)(format); default: return v => v; } } /** * Format epoch milliseconds with a format string * @type {typeof import('./data-utils').datetimeFormatter} timezone */ export function datetimeFormatter(timezone) { return timezone ? format => ts => moment .utc(ts) .tz(timezone) .format(format) : format => ts => moment.utc(ts).format(format); }