import { parse , evaluate , DocumentNode , Node , ObjectNode , } from '@humanwhocodes/momoa' ; import { GeoJSON } from 'geojson' ; import { HintError , makeIssue } from './errors' ; import { GeoJSONTypeSet , GEOJSON_TYPES , GEOJSON_GEOMETRY_TYPES , GEOJSON_GEOMETRY_TYPES_EX_GEOMETRY_COLLECTION , GEOJSON_FEATURE_TYPE , HintIssue , Ctx , } from './types' ; import { getType } from './get_type' ; import { getMemberValue } from './get_member_value' ; import { getArray } from './get_array' ; import { getObject } from './get_object' ; import { enforcePosition } from './enforce_position' ; import { checkDuplicateKeys } from './check_duplicate_keys' ; import { enforcePositionArray , enforcePositionArray2 , enforcePositionArray3 , } from './enforce_position_array' ; import { enforceBbox } from './enforce_bbox' ; import { forbidConfusingProperties } from './forbid_confusing_properties' ; type Checker = ( ctx : Ctx , node : ObjectNode ) => void ; function getCoordinates ( ctx , node ) { const coordinatesMember = getMemberValue ( ctx , node , 'coordinates' ) ; if ( ! coordinatesMember ) return null ; return getArray ( ctx , coordinatesMember ) ; } const checkGeometryShared = ( ctx , node ) => { enforceBbox ( ctx , node ) ; forbidConfusingProperties ( ctx , node , 'Geometry' ) ; } ; const checkLineString = ( ctx , node ) => { enforcePositionArray ( ctx , getCoordinates ( ctx , node ) , 'LineString' ) ; checkGeometryShared ( ctx , node ) ; } ; const checkMultiLineString = ( ctx , node ) => { enforcePositionArray2 ( ctx , getCoordinates ( ctx , node ) , 'LineString' ) ; checkGeometryShared ( ctx , node ) ; } ; const checkPolygon = ( ctx , node ) => { enforcePositionArray2 ( ctx , getCoordinates ( ctx , node ) , 'Polygon' ) ; checkGeometryShared ( ctx , node ) ; } ; const checkMultiPolygon = ( ctx , node ) => { enforcePositionArray3 ( ctx , getCoordinates ( ctx , node ) , 'Polygon' ) ; checkGeometryShared ( ctx , node ) ; } ; const checkPoint = ( ctx , node ) => { enforcePosition ( ctx , getCoordinates ( ctx , node ) ) ; checkGeometryShared ( ctx , node ) ; } ; const checkMultiPoint = ( ctx , node ) => { enforcePositionArray ( ctx , getCoordinates ( ctx , node ) ) ; checkGeometryShared ( ctx , node ) ; } ; const checkGeometryCollection = ( ctx , node ) => { checkGeometryShared ( ctx , node ) ; const geometriesMember = getArray ( ctx , getMemberValue ( ctx , node , 'geometries' ) ) ; if ( ! geometriesMember ) return ; for ( const element of geometriesMember . elements ) { checkObject ( ctx , element , GEOJSON_GEOMETRY_TYPES_EX_GEOMETRY_COLLECTION ) ; } } ; const checkFeature = ( ctx , node ) => { forbidConfusingProperties ( ctx , node , 'Feature' ) ; const geometryMember = getMemberValue ( ctx , node , 'geometry' ) ; enforceBbox ( ctx , node ) ; if ( geometryMember ?. type !== 'Null' ) { const geometry = getObject ( ctx , geometryMember ) ; if ( geometry ) checkObject ( ctx , geometry , GEOJSON_GEOMETRY_TYPES ) ; } const idMember = node . members . find ( ( member ) => { return member . name . value === 'id' ; } ) ; if ( idMember && ! ( idMember . value . type === 'String' || idMember . value . type === 'Number' ) ) { ctx . issues . push ( makeIssue ( `The Feature id must be a string or number.` , node ) ) ; } const properties = getMemberValue ( ctx , node , 'properties' ) ; if ( ! properties ) { ctx . issues . push ( makeIssue ( `The properties member is missing.` , node ) ) ; return ; } const { type } = properties ; if ( ! ( type === 'Object' || type === 'Null' ) ) { ctx . issues . push ( makeIssue ( `The Feature properties member can be an object or null.` , node ) ) ; } } ; const checkFeatureCollection = ( ctx , node ) => { forbidConfusingProperties ( ctx , node , 'FeatureCollection' ) ; const featuresMember = getArray ( ctx , getMemberValue ( ctx , node , 'features' ) ) ; if ( ! featuresMember ) return ; for ( const feature of featuresMember . elements ) { const beforeCount = ctx . issues . length ; const obj = getObject ( ctx , feature ) ; if ( obj ) { getType ( ctx , obj , GEOJSON_FEATURE_TYPE ) ; checkFeature ( ctx , obj ) ; } ctx . valid . push ( ctx . issues . length === beforeCount ? undefined : ctx . issues . slice ( beforeCount ) ) ; } } ; const CHECKERS = { LineString : checkLineString , MultiLineString : checkMultiLineString , Polygon : checkPolygon , MultiPolygon : checkMultiPolygon , Point : checkPoint , MultiPoint : checkMultiPoint , GeometryCollection : checkGeometryCollection , Feature : checkFeature , FeatureCollection : checkFeatureCollection , } ; function checkObject ( ctx , node , typeSet = GEOJSON_TYPES ) { const { type , objectNode } = getType ( ctx , node , typeSet ) ; if ( ! ( type && objectNode ) ) return ; checkDuplicateKeys ( ctx , objectNode ) ; CHECKERS [ type ] ( ctx , objectNode ) ; } function checkInternal ( jsonStr ) { const ctx = { issues : [ ] , valid : [ ] , } ; let ast ; try { ast = parse ( jsonStr , { ranges : true , } ) ; checkObject ( ctx , ast . body ) ; } catch ( e ) { ctx . issues . push ( { message : `Invalid JSON: ${ e . message }` , from : 0 , to : 0 , severity : 'error' , } ) ; } return { ast , ctx } ; } export const getIssues = ( jsonStr ) : HintIssue [ ] => { return checkInternal ( jsonStr ) . ctx . issues ; } ; export const check = ( jsonStr ) : GeoJSON => { const { ctx , ast } = checkInternal ( jsonStr ) ; if ( ctx . issues . length || ! ast ) throw new HintError ( ctx . issues ) ; return evaluate ( ast ) ; } ; export const scavenge = ( jsonStr ) : { result : GeoJSON ; rejected : Array < { feature : any ; reasons : HintIssue [ ] ; } > ; } => { const { ctx : { issues , valid } , ast , } = checkInternal ( jsonStr ) ; if ( ! ast ) throw new HintError ( issues ) ; const result = evaluate ( ast ) as GeoJSON ; if ( ! issues . length ) return { result , rejected : [ ] } ; if ( result . type === 'FeatureCollection' ) { return { result : { ... result , features : result . features . filter ( ( _ , i ) => { return valid [ i ] === undefined ; } ) , } , rejected : result . features . flatMap ( ( feature , i ) => { const reasons = valid [ i ] ; if ( reasons ) { return [ { feature , reasons , } , ] ; } return [ ] ; } ) , } ; } throw new HintError ( issues ) ; } ; export { HintError , HintIssue } ;