// TODO: This file was created by bulk-decaffeinate. // Sanity-check the conversion and remove this comment. // Utilities for db handling import _ from "lodash" import async from "async" import bowser from "bowser" import { compileDocumentSelector, compileSort } from "./selector" // Select appropriate local database, prefering IndexedDb, then WebSQLDb, then LocalStorageDb, then MemoryDb export function autoselectLocalDb(options: any, success: any, error: any) { // Here due to browserify circularity quirks const IndexedDb = require("./IndexedDb") const WebSQLDb = require("./WebSQLDb") const LocalStorageDb = require("./LocalStorageDb") const MemoryDb = require("./MemoryDb") // Get browser capabilities const { browser } = bowser // Always use WebSQL in cordova if (window.cordova) { console.log("Selecting WebSQLDb for Cordova") return new WebSQLDb(options, success, error) } // Use WebSQL in Android, iOS, Chrome, Safari, Opera, Blackberry if (browser.android || browser.ios || browser.chrome || browser.safari || browser.opera || browser.blackberry) { console.log("Selecting WebSQLDb for browser") return new WebSQLDb(options, success, error) } // Use IndexedDb on Firefox >= 16 if (browser.firefox && browser.version >= 16) { console.log("Selecting IndexedDb for browser") return new IndexedDb(options, success, error) } // Use Local Storage otherwise console.log("Selecting LocalStorageDb for fallback") return new LocalStorageDb(options, success, error) } // Migrates a local database's pending upserts and removes from one database to another // Useful for upgrading from one type of database to another export function migrateLocalDb(fromDb: any, toDb: any, success: any, error: any) { // Migrate collection using a HybridDb // Here due to browserify circularity quirks const HybridDb = require("./HybridDb") const hybridDb = new HybridDb(fromDb, toDb) for (let name in fromDb.collections) { const col = fromDb.collections[name] if (toDb[name]) { hybridDb.addCollection(name) } } return hybridDb.upload(success, error) } // Processes a find with sorting and filtering and limiting export function processFind(items: any, selector: any, options: any) { let filtered = _.filter(_.values(items), compileDocumentSelector(selector)) // Handle geospatial operators filtered = processNearOperator(selector, filtered) filtered = processGeoIntersectsOperator(selector, filtered) if (options && options.sort) { filtered.sort(compileSort(options.sort)) } if (options && options.limit) { filtered = _.first(filtered, options.limit) } // Clone to prevent accidental updates, or apply fields if present if (options && options.fields) { // For each item filtered = _.map(filtered, function (item: any) { let field, obj, path, pathElem item = _.cloneDeep(item) const newItem = {} if (_.first(_.values(options.fields)) === 1) { // Include fields for (field of _.keys(options.fields).concat(["_id"])) { path = field.split(".") // Determine if path exists obj = item for (pathElem of path) { if (obj) { obj = obj[pathElem] } } if (obj == null) { continue } // Go into path, creating as necessary let from = item let to = newItem for (pathElem of _.initial(path)) { to[pathElem] = to[pathElem] || {} // Move inside to = to[pathElem] from = from[pathElem] } // Copy value to[_.last(path)] = from[_.last(path)] } return newItem } else { // Exclude fields for (field of _.keys(options.fields).concat(["_id"])) { path = field.split(".") // Go inside path obj = item for (pathElem of _.initial(path)) { if (obj) { obj = obj[pathElem] } } // If not there, don't exclude if (obj == null) { continue } delete obj[_.last(path)] } return item } }) } else { filtered = _.map(filtered, (doc: any) => _.cloneDeep(doc)) } return filtered } // Creates a unique identifier string export function createUid() { return "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, function (c) { const r = (Math.random() * 16) | 0 const v = c === "x" ? r : (r & 0x3) | 0x8 return v.toString(16) }) } function processNearOperator(selector: any, list: any) { for (var key in selector) { var value = selector[key] if (value != null && value["$near"]) { var geo = value["$near"]["$geometry"] if (geo.type !== "Point") { break } list = _.filter(list, (doc: any) => doc[key] && doc[key].type === "Point") // Get distances let distances = _.map(list, (doc: any) => ({ doc, distance: getDistanceFromLatLngInM( geo.coordinates[1], geo.coordinates[0], doc[key].coordinates[1], doc[key].coordinates[0] ) })) // Filter non-points distances = _.filter(distances, (item: any) => item.distance >= 0) // Sort by distance distances = _.sortBy(distances, "distance") // Filter by maxDistance if (value["$near"]["$maxDistance"]) { distances = _.filter(distances, (item: any) => item.distance <= value["$near"]["$maxDistance"]) } // Limit to 100 distances = _.first(distances, 100) // Extract docs list = _.map(distances, "doc") } } return list } // Very simple polygon check. Assumes that is a square function pointInPolygon(point: any, polygon: any) { // Check that first == last if (!_.isEqual(_.first(polygon.coordinates[0]), _.last(polygon.coordinates[0]))) { throw new Error("First must equal last") } // Check bounds if ( point.coordinates[0] < Math.min.apply( this, _.map(polygon.coordinates[0], (coord: any) => coord[0]) ) ) { return false } if ( point.coordinates[1] < Math.min.apply( this, _.map(polygon.coordinates[0], (coord: any) => coord[1]) ) ) { return false } if ( point.coordinates[0] > Math.max.apply( this, _.map(polygon.coordinates[0], (coord: any) => coord[0]) ) ) { return false } if ( point.coordinates[1] > Math.max.apply( this, _.map(polygon.coordinates[0], (coord: any) => coord[1]) ) ) { return false } return true } // From http://www.movable-type.co.uk/scripts/latlong.html function getDistanceFromLatLngInM(lat1: any, lng1: any, lat2: any, lng2: any) { const R = 6371000 // Radius of the earth in m const dLat = deg2rad(lat2 - lat1) // deg2rad below const dLng = deg2rad(lng2 - lng1) const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2) const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) const d = R * c // Distance in m return d } function deg2rad(deg: any) { return deg * (Math.PI / 180) } function processGeoIntersectsOperator(selector: any, list: any) { for (var key in selector) { const value = selector[key] if (value != null && value["$geoIntersects"]) { var geo = value["$geoIntersects"]["$geometry"] if (geo.type !== "Polygon") { break } // Check within for each list = _.filter(list, function (doc: any) { // Reject non-points if (!doc[key] || doc[key].type !== "Point") { return false } // Check polygon return pointInPolygon(doc[key], geo) }) } } return list }