Skip to content

Commit

Permalink
F/detect types (#15)
Browse files Browse the repository at this point in the history
* make room for post processing query

* first very ugly cut at field detection

* translate date fields to unix time for esri

* update docs

* remove stray comment

* remove console log
  • Loading branch information
Daniel Fenton committed Jun 15, 2017
1 parent 6f47f01 commit e3004ce
Show file tree
Hide file tree
Showing 11 changed files with 282 additions and 66 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,11 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased
### Added
- Detect field types if they are not passed in with `geojson.metadata`
- Translate ISO Date Strings to Unix Timestamps when `options.toEsri` is true

## [1.9.0] - 05-24-2017
### Added
* Support outSR for polygons and lines
Expand Down
35 changes: 34 additions & 1 deletion README.md
Expand Up @@ -102,7 +102,40 @@ An array of fields use to sort the output
Can be an epsg code, an ogc wkt or an esri wkt. This parameter controls how the geometry will be projected to another coordinate system.

### `toEsri`
If true, the object returned will be an esri feature collection
If true, the object returned will be an esri feature collection.

Winnow will automatically determine field types from the first feature passed in. If a given attribute is null, Winnow will assume it is a string.

You can also pass in a metadata object that describes the fields in the feature collection. This is recommended if you know your schema ahead of time

e.g.

```js
{
type: 'FeatureCollection',
features: [],
metadata: {
fields: [
{
name: 'SomeDateField',
type: 'Date'
},
{
name: 'SomeDoubleField',
type: 'Double'
},
{
name: 'SomeIntegerField',
type: 'Integer'
},
{
name: 'SomeStringField',
type: 'String'
}
]
}
}
```

## `winnow.prepareQuery`
Returns a function that can be applied directly to a feature collection object, an array of features, or a single feature. Useful when you want to pass a stream of features through a filter.
Expand Down
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -35,6 +35,7 @@
"alasql": "^0.4.0",
"highland": "^3.0.0-beta.3",
"lodash": "^4.17.4",
"moment": "^2.18.1",
"ngeohash": "^0.6.0",
"proj4": "^2.3.17",
"terraformer": "^1.0.7"
Expand Down
27 changes: 27 additions & 0 deletions src/detect-fields-type.js
@@ -0,0 +1,27 @@
const moment = require('moment')
const DATE_FORMATS = [moment.ISO_8601]

module.exports = function (properties) {
return Object.keys(properties).map(name => {
return {
name,
type: detectType(properties[name])
}
})
}

function detectType (value) {
var type = typeof value

if (type === 'number') {
return isInt(value) ? 'Integer' : 'Double'
} else if (type && moment(value, DATE_FORMATS, true).isValid()) {
return 'Date'
} else {
return 'String'
}
}

function isInt (value) {
return Math.round(value) === value
}
47 changes: 42 additions & 5 deletions src/index.js
Expand Up @@ -17,14 +17,14 @@ Winnow.query = function (input, options = {}) {
features = input.features
}

options = Options.prepare(options)
options = Options.prepare(options, features)

// The following two functions are query preprocessing steps
const query = Query.create(options)
const params = Query.params(features, options)
if (process.env.NODE_ENV === 'test') console.log(query, options)
const filtered = sql(query, params)
return finishQuery(filtered, options)

if (options.aggregates) return aggregateQuery(features, query, options)
else if (options.limit) return limitQuery(features, query, options)
else return standardQuery(features, query, options)
}

Winnow.prepareQuery = function (options) {
Expand Down Expand Up @@ -84,4 +84,41 @@ function finishQuery (features, options) {
}
}

function aggregateQuery (features, query, options) {
const params = Query.params(features, options)
const filtered = sql(query, params)
return finishQuery(filtered, options)
}

function limitQuery (features, query, options) {
const filtered = []
features.some(feature => {
const result = processQuery(feature, query, options)
if (result) filtered.push(result)
return filtered.length === options.limit
})
return finishQuery(filtered, options)
}

function standardQuery (features, query, options) {
const filtered = features.reduce((filteredFeatures, feature, i) => {
// TODO used passed in fields if available
const result = processQuery(feature, query, options)
if (result) filteredFeatures.push(result)
return filteredFeatures
}, [])
return finishQuery(filtered, options)
}

function processQuery (feature, query, options) {
const params = Query.params([feature], options)
const result = sql(query, params)[0]
if (options.dateFields.length && options.toEsri) {
options.dateFields.forEach(field => {
result.attributes[field] = new Date(result.attributes[field]).getTime()
})
}
return result
}

module.exports = Winnow
27 changes: 25 additions & 2 deletions src/options.js
Expand Up @@ -3,14 +3,16 @@ const convertFromEsri = require('./geometry/convert-from-esri')
const transformArray = require('./geometry/transform-array')
const transformEnvelope = require('./geometry/transform-envelope')
const projectCoordinates = require('./geometry/project-coordinates')
const detectFieldsType = require('./detect-fields-type')
const esriPredicates = {
esriSpatialRelContains: 'ST_Contains',
esriSpatialRelWithin: 'ST_Within',
esriSpatialRelIntersects: 'ST_Intersects'
}

function prepare (options) {
return _.merge({}, options, {
function prepare (options, features) {
const prepared = _.merge({}, options, {
collection: normalizeCollection(options, features),
where: normalizeWhere(options),
geometry: normalizeGeometry(options),
spatialPredicate: normalizeSpatialPredicate(options),
Expand All @@ -22,6 +24,27 @@ function prepare (options) {
offset: normalizeOffset(options),
projection: normalizeProjection(options)
})
prepared.dateFields = normalizeDateFields(prepared.collection)
return prepared
}

function normalizeCollection (options, features) {
if (!options.collection) return undefined
const collection = _.cloneDeep(options.collection)
const metadata = collection.metadata || {}
if (!metadata.fields) metadata.fields = detectFieldsType(features[0].properties)
collection.metadata = metadata
return collection
}

function normalizeDateFields (collection) {
let dateFields = []
if (collection && collection.metadata && collection.metadata.fields) {
collection.metadata.fields.forEach((field, i) => {
if (field.type === 'Date') dateFields.push(field.name)
})
}
return dateFields
}

function normalizeWhere (options) {
Expand Down

0 comments on commit e3004ce

Please sign in to comment.