UNPKG

kepler.gl

Version:

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

161 lines (135 loc) 5.46 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 {Analyzer, DATA_TYPES} from 'type-analyzer'; import {getSampleData, timeToUnixMilli, notNullorUndefined} from 'utils/data-utils'; import {parseGeoJsonRawFeature, getGeojsonFeatureTypes} from 'layers/geojson-layer/geojson-utils'; /** * Parse geojson from string * @param {array} geojson feature object values * @returns {boolean} whether the geometry coordinates has length of 4 */ export function coordHasLength4(samples) { let hasLength4 = true; for (let i = 0; i < samples.length; i += 1) { hasLength4 = !samples[i].geometry.coordinates.find(c => c.length < 4); if (!hasLength4) { break; } } return hasLength4; } /** * Check whether geojson linestring's 4th coordinate is 1) not timestamp 2) unix time stamp 3) real date time * @param {array} data array to be tested if its elements are timestamp * @returns {string} the type of timestamp: unix/datetime/invalid(not timestamp) */ export function containValidTime(timestamps) { const formattedTimeStamps = timestamps.map(ts => ({ts})); const ignoredDataTypes = Object.keys(DATA_TYPES).filter( type => ![DATA_TYPES.TIME, DATA_TYPES.DATETIME].includes(type) ); // ignore all types but TIME to improve performance const analyzedType = Analyzer.computeColMeta(formattedTimeStamps, [], {ignoredDataTypes})[0]; if (!analyzedType || analyzedType.category !== 'TIME') { return false; } return analyzedType; } /** * Check if geojson features are trip layer animatable by meeting 3 conditions * @param {array} features array of geojson feature objects * @returns {boolean} whether it is trip layer animatable */ export function isTripGeoJsonField(allData = [], field) { if (!allData.length) { return false; } const getValue = d => d[field.tableFieldIndex - 1]; const maxCount = 10000; const sampleRawFeatures = allData.length > maxCount ? getSampleData(allData, maxCount, getValue) : allData.map(getValue); const features = sampleRawFeatures.map(parseGeoJsonRawFeature).filter(f => f); const featureTypes = getGeojsonFeatureTypes(features); // condition 1: contain line string if (!featureTypes.line) { return false; } // condition 2:sample line strings contain 4 coordinates if (!coordHasLength4(features)) { return false; } // condition 3:the 4th coordinate of the first feature line strings is valid time const tsHolder = features[0].geometry.coordinates.map(coord => coord[3]); return Boolean(containValidTime(tsHolder)); } /** * Get unix timestamp from animatable geojson for deck.gl trip layer * @param {Array<Object>} dataToFeature array of geojson feature objects, can be null * @returns {Array<Number>} unix timestamp in milliseconds */ export function parseTripGeoJsonTimestamp(dataToFeature) { // Analyze type based on coordinates of the 1st lineString // select a sample trip to analyze time format const empty = {dataToTimeStamp: [], animationDomain: null}; const sampleTrip = dataToFeature.find( f => f && f.geometry && f.geometry.coordinates && f.geometry.coordinates.length >= 3 ); // if no valid geometry if (!sampleTrip) { return empty; } const analyzedType = containValidTime(sampleTrip.geometry.coordinates.map(coord => coord[3])); if (!analyzedType) { return empty; } const {format} = analyzedType; const getTimeValue = coord => coord && notNullorUndefined(coord[3]) ? timeToUnixMilli(coord[3], format) : null; const dataToTimeStamp = dataToFeature.map(f => f && f.geometry && Array.isArray(f.geometry.coordinates) ? f.geometry.coordinates.map(getTimeValue) : null ); const animationDomain = getAnimationDomainFromTimestamps(dataToTimeStamp); return {dataToTimeStamp, animationDomain}; } function findMinFromSorted(list = []) { return list.find(notNullorUndefined) || null; } function findMaxFromSorted(list = []) { let i = list.length - 1; while (i > 0) { if (notNullorUndefined(list[i])) { return list[i]; } i--; } return null; } export function getAnimationDomainFromTimestamps(dataToTimeStamp = []) { return dataToTimeStamp.reduce( (accu, tss) => { accu[0] = Math.min(accu[0], findMinFromSorted(tss)); accu[1] = Math.max(accu[1], findMaxFromSorted(tss)); return accu; }, [Infinity, -Infinity] ); }