import _ from 'lodash' ; import { SELECTION , DEFAULT_SOURCE , DEFAULT_CHART_TYPE , LINK_OF } from './def' ; interface NodeDescriptor { id : string ; } interface SourceNodeDescriptor extends NodeDescriptor { isSelection ? : boolean ; } interface TargetNodeDescriptor extends NodeDescriptor { isCreate ? : boolean ; } interface VisualsSpecification { assignment ? : { [ prop ] : string | number ; } ; encoding ? : { column : string ; type : string ; scale : string | number [ ] ; } ; } interface FilterSpecification { column : string ; pattern ? : string ; sampling ? : 'random' ; extremum ? : 'minimum' | 'maximum' ; range ? : { min ? : number | string ; max ? : number | string ; } ; amount ? : number ; amountType ? : 'percentage' | 'count' ; } interface SetOperatorSpecification { type : string ; nodes : SourceNodeDescriptor [ ] ; } interface ExtractSpecification { column : string ; } interface LinkSpecification { extractColumn ? : string ; filterColumn ? : string ; } export interface EdgeSpecification { type : 'connect' | 'disconnect' ; nodes : Array < SourceNodeDescriptor | TargetNodeDescriptor > ; } export interface NodeSpecification { type : 'remove' ; nodes : NodeDescriptor [ ] ; } export interface QueryValue { loadDataset ? : string ; autoLayout ? : boolean ; columns ? : string [ ] ; seriesColumn ? : string ; groupByColumn ? : string ; visuals ? : VisualsSpecification [ ] ; filters ? : FilterSpecification [ ] ; extract ? : ExtractSpecification ; link ? : LinkSpecification ; setOperator ? : SetOperatorSpecification ; edge ? : EdgeSpecification ; node ? : NodeSpecification ; source ? : SourceNodeDescriptor [ ] ; target ? : TargetNodeDescriptor [ ] ; highlight ? : boolean ; select ? : boolean ; undo ? : boolean ; redo ? : boolean ; } /**
 * Parses an amount string, i.e. count or percentage.
 */ const parseAmount = ( value ) : { amount : number ; amountType : 'count' | 'percentage' ; } => { const amountStr = value ; let amount ; let amountType = 'percentage' ; if ( amountStr . match ( /_\%$/ ) ) { amount = + amountStr . match ( /^(.*)_\%$/ ) [ 1 ] * 100 ; } else if ( amountStr . match ( /\%$/ ) ) { amount = + amountStr . match ( /^(.*)\%$/ ) [ 1 ] ; } else { amount = + amountStr ; amountType = 'count' ; } return { amount , amountType } ; } ; /**
 * Adds a series column specification to the result.
 */ const addSeriesColumn = ( result , values ) => { result . seriesColumn = values [ 0 ] ; } ; /**
 * Adds a group by column specification to the result.
 */ const addGroupByColumn = ( result , values ) => { result . groupByColumn = values [ 0 ] ; } ; /**
 * Adds a filter specification to the result.
 */ const addFilter = ( result , values ) => { if ( ! result . filters ) { result . filters = [ ] ; } const type = values [ 1 ] ; const spec = { column : values [ 0 ] , } ; if ( type === undefined ) { result . filters . push ( spec ) ; return ; } values = values . slice ( 2 ) ; if ( type === 'pattern' || type === '=' ) { spec . pattern = values [ 0 ] ; } else if ( type === 'range' || type === '[]' || type . indexOf ( '=' ) !== - 1 ) { if ( type === 'range' || type === '[]' ) { spec . range = { min : values [ 0 ] , max : values [ 1 ] } ; } else { spec . range = { } ; values = [ type ] . concat ( values ) ; while ( values . length ) { if ( values [ 0 ] === '>=' ) { spec . range . min = values [ 1 ] ; } else if ( values [ 0 ] === '<=' ) { spec . range . max = values [ 1 ] ; } values = values . slice ( 2 ) ; } } } else if ( type === 'extremum' || type === 'sampling' ) { if ( type === 'extremum' ) { spec . extremum = values [ 0 ] === 'min' ? 'minimum' : 'maximum' ; values . shift ( ) ; } else { spec . sampling = 'random' ; } _ . extend ( spec , parseAmount ( values [ 0 ] ) ) ; values . shift ( ) ; if ( values [ 0 ] === 'group_by' ) { addGroupByColumn ( result , values . slice ( 1 ) ) ; } } result . filters . push ( spec ) ; } ; /**
 * Adds visuals configuration to the result.
 */ const addVisuals = ( result , values ) => { if ( ! result . visuals ) { result . visuals = [ ] ; } let entry = { } ; if ( values [ 0 ] === 'assignment' ) { const visuals = { } ; for ( let index = 1 ; index < values . length ; index += 2 ) { const prop = values [ index ] ; const value = values [ index + 1 ] ; if ( value === '+' || value === '-' || prop === 'color' || prop === 'border' ) { visuals [ prop ] = value ; } else { visuals [ prop ] = + value ; } } entry = { assignment : visuals } ; } else if ( values [ 0 ] === 'encoding' ) { entry = { encoding : { column : values [ 1 ] , type : values [ 2 ] , scale : values [ 4 ] !== undefined ? [ + values [ 3 ] , + values [ 4 ] ] : values [ 3 ] , } , } ; } else { console . error ( 'cannot parse visuals values' , values ) ; } result . visuals . push ( entry ) ; } ; /**
 * Adds a source node specification to the result.
 */ const addSource = ( result , values ) => { if ( ! result . source ) { result . source = [ ] ; } if ( ! values . length ) { return ; } while ( values . length > 0 ) { if ( values [ 0 ] !== SELECTION ) { const sourceId = values [ 0 ] ; values . shift ( ) ; let isSelection ; if ( values [ 0 ] === SELECTION ) { values . shift ( ) ; isSelection = true ; } if ( result . source . length && result . source [ 0 ] . id === DEFAULT_SOURCE ) { result . source [ 0 ] . id = sourceId ; result . source [ 0 ] . isSelection = result . source [ 0 ] . isSelection || isSelection ? true : undefined ; addSource ( result , values ) ; return ; } else { result . source . push ( { id : sourceId , isSelection , } ) ; } } else { values . shift ( ) ; result . source . push ( { id : DEFAULT_SOURCE , isSelection : true , } ) ; } } } ; /**
 * Adds a target node specification to the result.
 */ const addTarget = ( result , values ) => { if ( ! result . target ) { result . target = [ ] ; } const isCreate = values [ 1 ] !== 'no_create' ; if ( values [ 0 ] !== DEFAULT_CHART_TYPE ) { for ( const target of result . target ) { if ( target . id === DEFAULT_CHART_TYPE ) { target . id = values [ 0 ] ; return ; } } } result . target . push ( { id : values [ 0 ] , isCreate , } ) ; } ; const getNodes = ( values ) : Array < SourceNodeDescriptor | TargetNodeDescriptor > => { const nodes = [ ] ; while ( values . length ) { nodes . push ( { id : values [ 0 ] , isSelection : values [ 1 ] === SELECTION ? true : undefined , } ) ; values . shift ( ) ; if ( values [ 0 ] === SELECTION ) { values . shift ( ) ; } } return nodes ; } ; /**
 * Adds set operator specification to the result.
 */ const addSetOperator = ( result , values ) => { const type = values [ 0 ] ; values . shift ( ) ; result . setOperator = { type , nodes : getNodes ( values ) , } ; } ; /**
 * Adds a list of columns to the result.
 * The column specification may contain series and groupBy column specifications.
 */ const addColumns = ( result , values ) => { if ( result . columns ) { console . error ( 'duplicate configuration of columns' ) ; } result . columns = values [ 0 ] . split ( '&' ) ; values = values . slice ( 1 ) ; while ( values . length ) { if ( values [ 0 ] === 'series' ) { addSeriesColumn ( result , values . slice ( 1 ) ) ; } else if ( values [ 0 ] === 'group_by' ) { addGroupByColumn ( result , values . slice ( 1 ) ) ; } else if ( values [ 0 ] === SELECTION ) { addSource ( result , [ SELECTION ] ) ; } else { console . error ( `unknown columns specification ${ values [ 0 ] }` ) ; break ; } values = values . slice ( 2 ) ; } } ; /**
 * Turns on highlight flag in the result.
 */ const addHighlight = ( result , values ) => { result . highlight = true ; } ; /**
 * Turns on select flag in the result.
 */ const addSelect = ( result , values ) => { result . select = true ; } ; /**
 * Adds constant extraction to the result.
 */ const addExtract = ( result , values ) => { result . extract = { column : values [ 0 ] , } ; if ( values [ 1 ] === SELECTION ) { addSource ( result , [ SELECTION ] ) ; } } ; /**
 * Handles edge connection and disconnection.
 */ const addEdge = ( result , values ) => { const type = values [ 0 ] ; if ( type !== 'connect' && type !== 'disconnect' ) { console . error ( 'addEdge type is neither "connect" nor "disconnect"' ) ; return ; } values . shift ( ) ; result . edge = { type , nodes : getNodes ( values ) , } ; } ; /**
 * Handles node removal.
 */ const addNode = ( result , values ) => { const type = values [ 0 ] ; if ( type !== 'remove' ) { console . error ( 'addEdge type can only be "remove"' ) ; return ; } values . shift ( ) ; result . node = { type , nodes : getNodes ( values ) , } ; } ; /**
 * Adds node linking to the result.
 */ const addLink = ( result , values ) => { let extractColumn = '' ; let filterColumn = '' ; const columns = [ ] ; while ( values . length ) { if ( values [ 0 ] === SELECTION ) { addSource ( result , [ values [ 0 ] ] ) ; values . shift ( ) ; } else { const column = values [ 0 ] ; if ( values . length >= 3 && values [ 1 ] === LINK_OF ) { addSource ( result , [ values [ 2 ] ] ) ; values = values . slice ( 3 ) ; } else { values . shift ( ) ; } columns . push ( column ) ; } } if ( columns . length >= 2 ) { extractColumn = columns [ 0 ] ; filterColumn = columns [ 1 ] ; } else { extractColumn = columns [ 0 ] ; filterColumn = undefined ; } result . link = { extractColumn , filterColumn } ; } ; /**
 * Parses the query value (string) into a JSON object.
 */ export const parseQueryValue = ( value ) : QueryValue => { const pairStrings = value . replace ( /\s+/ , '' ) . split ( ';' ) ; const result = { } ; for ( const str of pairStrings ) { const tokens = str . split ( ':' ) ; const key = tokens [ 0 ] ; const values = tokens . slice ( 1 ) ; switch ( key ) { case 'set' : addSetOperator ( result , values ) ; break ; case 'filter' : addFilter ( result , values ) ; break ; case 'visuals' : addVisuals ( result , values ) ; break ; case 'source' : addSource ( result , values ) ; break ; case 'target' : addTarget ( result , values ) ; break ; case 'load' : result . loadDataset = values [ 0 ] ; break ; case 'auto-layout' : result . autoLayout = true ; break ; case 'columns' : addColumns ( result , values ) ; break ; case 'series' : addSeriesColumn ( result , values ) ; break ; case 'group_by' : addGroupByColumn ( result , values ) ; break ; case 'highlight' : addHighlight ( result , values ) ; break ; case 'select' : addSelect ( result , values ) ; break ; case 'extract' : addExtract ( result , values ) ; break ; case 'link' : addLink ( result , values ) ; break ; case 'edge' : addEdge ( result , values ) ; break ; case 'node' : addNode ( result , values ) ; break ; case 'undo' : result . undo = true ; break ; case 'redo' : result . redo = true ; break ; } } return result ; } ;