/**
* @module stateManager
* @memberof app.common
* @description
*
* The `stateManager` factory is a service controlling states content.
*
*/
angular
.module('app.core')
.factory('stateManager', stateManager);
function stateManager($timeout, $translate, events, constants, commonService, modelManager) {
const service = {
getState,
goNoGoPreview,
validateModel
};
const _state = {};
return service;
/*********/
/**
* Get state object
* @function getState
* @param {Object} modelName the model/form name to get state on
* @return {Object} the state object in JSON
*/
function getState(modelName) {
const link = constants.schemas
.indexOf(`${modelName}.[lang].json`) + 1;
// create state object used by the summary section
_state[modelName] = { 'key': modelName,
'title': $translate.instant(`app.section.${modelName}`),
'valid': null,
'expand': false,
'masterlink': link,
'hlink': link,
'advance': false,
'stype': '',
'shlink': '',
items: [] };
return _state[modelName];
}
/**
* Check validity of state sections
* @function goNoGoPreview
* @return {Boolean} the go or no go
*/
function goNoGoPreview() {
let goNoGo = false; // Default
// State defined
if (Object.keys(_state).length === 0
&& _state.constructor === Object) {
goNoGo = false;
} else {
let validity = [];
const names = Object.getOwnPropertyNames(_state);
for (let item of names) {
validity.push(_state[item].valid);
}
goNoGo = !(validity.includes(false) || validity.includes(null));
}
return goNoGo;
}
/**
* Set state object with valid values from the form field validity
* @function validateModel
* @param {String} modelName the model/form name to get state on
* @param {Object} form the angular schema form active form
* @param {Array} arrForm the form as an array of objects
* @param {Object} model the model
*/
function validateModel(modelName, form, arrForm, model) {
const cleanForm = commonService.parseJSON(form);
// Since map is much more complicated we isolate it
if (modelName === 'map') {
const arrKeys = updateSummaryFormMap(_state[modelName], modelName, cleanForm);
// Generate state records for basemaps, layers, tileSchemas, extentSets and lodSets
setMapItemsState(_state[modelName], model, arrKeys);
} else {
updateSummaryForm(_state[modelName], modelName, cleanForm);
}
// TITLE SECTION
// Set each title
setTitle(_state[modelName], arrForm);
// Set custom title
setCustomTitles(_state[modelName], modelName);
// VALIDITY SECTION
// Set master element validity
setMasterValidity(_state[modelName]);
// UNDEFINED SECTION
// Undefined parameters
let modKeys = [modelName];
let modUndef = [];
searchUndefined(modKeys, model, modUndef);
// Remove keys that are not in the stateModel
// eg. both about.content and about.folderName exist in the model
// but just one in the stateModel. This is caused by the way we managed JSON schema 'OneOf'
// Should not be applied on list of elements [tileSchemas, extents, lods, basemaps, layers]
if (modelName === 'ui') {
let modUndefExist = [];
// Special case where we have to add aboutChoice key;
for (let i of modUndef) {
if (i[0].includes('about')) {
const index = i[0].indexOf('about') + 1;
i[0].splice(index, 0, 'aboutChoice');
}
}
removeNonExistent(_state[modelName], modelName, modUndef, modUndefExist);
updateValidity(_state[modelName], modelName, modUndefExist);
} else {
// Update validity based on undefined
updateValidity(_state[modelName], modelName, modUndef);
}
// ADVANCE SECTION
// Advance parameters
let adv = [];
listAdvanceHidden(arrForm, adv);
const advHidden = [].concat.apply([], adv);
// Set special style to hidden advance parameter
setAdvance(_state[modelName], modelName, advHidden);
}
/**
* Remove keys if they don't exist in the stateModel
* @function removeNonExistent
* @private
* @param {Object} stateModel the stateModel
* @param {String} modelName modelName
* @param {Array} keysIn list of original keys [[keys], valid]
* @param {Array} keysOut list of update keys [[keys], valid]
*/
function removeNonExistent(stateModel, modelName, keysIn, keysOut) {
const keysInUpd = addSpecific(keysIn, modelName);
for (let keys of keysInUpd) {
let keysCheck = keys[0].slice();
keysCheck.unshift(modelName);
let exist = [];
existInStateModel(stateModel, keysCheck, exist);
if (exist.includes(true)) keysOut.push(keys);
}
}
/**
* Look for the existence of a path in the stateModel
* @function existInStateModel
* @private
* @param {Object} stateModel the stateModel
* @param {Array} keys set of keys defining a path
* @param {Array} exist true if the path exist
*/
function existInStateModel(stateModel, keys, exist) {
if (stateModel.key === keys[0]) {
keys.shift();
if (keys.length === 0) {
exist.push(true);
} else if (stateModel.hasOwnProperty('items')) {
for (let item of stateModel.items) {
if (keys[0] === item.key) existInStateModel(item, keys, exist);
}
}
}
}
/**
* Set advance parameter in state model
* All element and sub-element will have 'advance' parameter
* set to true
* @function setAdvance
* @private
* @param {Object} stateModel the stateModel
* @param {String} modelName modelName
* @param {Array} arrKeys list of advance parameter keys
*/
function setAdvance(stateModel, modelName, arrKeys) {
if (arrKeys.includes(stateModel.key)) {
setStateValueDown(stateModel, 'advance', true)
} else if (stateModel.hasOwnProperty('items')) {
for (let item of stateModel.items) {
setAdvance(item, modelName, arrKeys);
}
}
}
/**
* Set a parameter value for an element of the state model
* and all is children
* @function setStateValueDown
* @private
* @param {Object} stateModel the stateModel
* @param {String} param parameter name
* @param {Object} value value to give to parameter
*/
function setStateValueDown(stateModel, param, value) {
stateModel[param] = value;
if (stateModel.hasOwnProperty('items')) {
for (let item of stateModel.items) {
setStateValueDown(item, param, value);
}
}
}
/**
* Set a parameter value for an element of the state model
* and all parents
* @function setStateValueUp
* @private
* @param {Object} stateModel the stateModel
* @param {Array} path path from root to element ['root','child','grandchild=element']
* @param {String} param parameter name
* @param {Object} value value to give to parameter
*/
function setStateValueUp(stateModel, path, param, value) {
stateModel[param] = value;
const target = path.shift();
if (path.length > 0) {
// Find proper target place
for (let [j, item] of stateModel.items.entries()) {
// Check if the target is a number
// if so compare with index instead of key
const isNumber = !isNaN(target);
if ((isNumber && j === parseInt(target)) || item.key === target) {
setStateValueUp(stateModel.items[j], path, param, value);
}
}
}
}
/**
* List undefined attributes
* @function searchUndefined
* @private
* @param {Array} modelKeys the model name path
* @param {Object} model the model
* @param {Array} arrKeys array of keys
*/
function searchUndefined(modelKeys, model, arrKeys) {
const dataType = commonService.whatsThat(model);
if (dataType === 'undefined') {
modelKeys.shift();
arrKeys.push([modelKeys, false]);
} else if(dataType !== 'null') {
const entries = Object.entries(model);
for (let el of entries) {
if (dataType === 'array' || dataType === 'object') {
const keys = modelKeys.concat(el[0]);
searchUndefined(keys, el[1], arrKeys);
}
}
}
}
/**
* Set undefined parameter in state model and update
* validity of upper hierarchy
* @function updateValidity
* @private
* @param {Object} stateModel the stateModel
* @param {String} modelName modelName
* @param {Array} undefKeys list of advance parameter keys
*/
function updateValidity(stateModel, modelName, undefKeys) {
const keysArrUpd = addSpecific(undefKeys, modelName);
for (let k of keysArrUpd) {
const path = k[0];
setStateValueUp(stateModel, path, 'valid', false);
}
}
/**
* Set new record for map items in state model
* @function setMapItemsState
* @private
* @param {Object} stateModel the stateModel
* @param {Object} model the model
* @param {Array} arrKeys array of object {key: [], valid: true | false}
*/
function setMapItemsState(stateModel, model, arrKeys) {
// baseMaps and layers
const setNames = [[2,'baseMaps'], [3, 'layers']];
const masterLink = constants.schemas
.indexOf(`map.[lang].json`) + 1;
for (let i of setNames) {
const items = model[i[1]];
const hlink = constants.subTabs.map.keys[i[0]].replace(/\./g, '-');
stateModel.items[i[0]]['items'] = [];
for (let [j, item] of items.entries()) {
const shlink = setItemId(hlink, i[1], j);
stateModel.items[i[0]]['items']
.push({ 'key': item.name,
'title': item.name,
items: [],
'valid': true,
'expand': false,
'masterlink': masterLink,
'hlink': hlink,
'advance': false,
'stype': 'element',
'shlink': shlink,
'type': 'object' });
}
}
// tilesSchemas, extents, lods
const setID = [[0,'tileSchemas', 'name'], [1, 'extentSets', 'id'], [2, 'lodSets', 'id']];
for (let i of setID) {
const items = model[i[1]];
const hlink = constants.subTabs.map.keys[0].replace(/\./g, '-');
stateModel.items[0].items[i[0]]['items'] = [];
for (let item of items) {
stateModel.items[0].items[i[0]]['items']
.push({ 'key': item[i[2]],
'title': item[i[2]],
items: [],
'valid': true,
'expand': false,
'masterlink': masterLink,
'hlink': hlink,
'advance': false,
'stype': 'element',
'shlink': '',
'type': 'object' });
}
}
}
/**
* Get validity from an item in a list
* Currently works only with baseMaps, layers, tileSchemas, extentSets, lodSets
* @function getValidityValue
* @private
* @param {String} key key to select the proper list
* @param {Number} index index in the form
* @param {Array} arrKeys array of object {key: [], valid: true | false}
* @return {Boolean} validity
*/
function getValidityValue(key, index, arrKeys) {
const validKey = arrKeys.filter(el => el[0][0] === key && el[0][1] === index.toString()).slice();
const validArr = validKey.map(el => {
el = el.slice(1);
return el;
}).slice();
return !validArr.includes(false);
}
/**
* List of hidden advance parameters
* @function listAdvanceHidden
* @private
* @param {Array} arrForm the form as an array of objects
* @param {Array} list list of hidden advance parameters
*/
function listAdvanceHidden(arrForm, list) {
arrForm.forEach(item => {
if (item.hasOwnProperty('htmlClass') && item.htmlClass === 'av-form-advance hidden') {
list.push(item.key);
}
if (item.hasOwnProperty('items')) {
listAdvanceHidden(item.items, list);
}
});
}
/**
* Retrieve the title corresponding to the provided key
* @function setTitle
* @private
* @param {Object} stateModel the stateModel
* @param {Array} arrForm the form as an array of objects
*/
function setTitle(stateModel, arrForm) {
if (stateModel.hasOwnProperty('key') && stateModel.title === '') {
findTitle(stateModel.key, stateModel, arrForm);
}
if (stateModel.hasOwnProperty('items')) {
stateModel.items.forEach(item => {
setTitle(item, arrForm);
});
}
}
/**
* Retrieve the title corresponding to the provided key
* @function setCustomTitles
* @private
* @param {Object} stateModel the stateModel
* @param {String} modelName modelName
*/
function setCustomTitles(stateModel, modelName) {
const customTitles = {
'map': ['form.map.extentlods'],
'ui': ['form.ui.general', 'form.ui.nav', 'form.ui.sidemenu'],
'services': ['form.service.urls'],
'language': [],
'version': []
};
customTitles[modelName].forEach(title => {
if (stateModel.hasOwnProperty('key') && stateModel.key === title) {
stateModel.title = $translate.instant(title);
}
if (stateModel.hasOwnProperty('items')) {
stateModel.items.forEach(item => {
setCustomTitles(item, modelName);
});
}
});
}
/**
* Search for the title corresponding to the provided key
* Ref: https://stackoverflow.com/questions/7837456/how-to-compare-arrays-in-javascript?page=1&tab=votes#tab-top
* @function findTitle
* @private
* @param {key} key the key to look for (could be an array)
* @param {Object} itemForm a form item
* @param {Array} arrForm the form as an array of objects
*/
function findTitle(key, itemForm, arrForm) {
arrForm.forEach(item => {
if (item.hasOwnProperty('key')) {
if (Array.isArray(item.key)) {
if (key === item.key[item.key.length - 1]) {
itemForm.title = item.title;
}
} else if (key === item.key) {
itemForm.title = item.title;
}
}
if (item.hasOwnProperty('items')) {
findTitle(key, itemForm, item.items);
}
});
}
/**
* Create state object from model/form for the map section of the summary panel
* @function updateSummaryFormMap
* @private
* @param {Object} state the state object in JSON
* @param {String} modelName the current model name
* @param {Object} form the form object in JSON
* @return {Array} arrKeys array of object {key: [], valid: true | false}
*/
function updateSummaryFormMap(state, modelName, form) {
let keysArr = [];
// loop trough form keys to create set of keys
$.each(form, key => {
if (key.startsWith('activeForm-')) {
// get all the keys to find the object
let keys = key.replace('--', '-').split('-').filter(n => n !== '' && n!== 'activeForm');
// remove first element of keys set if it is '0'
if (keys [0] === '0') keys.shift();
keysArr.push([keys, form[key].$valid]);
}
});
// Simplify keys set
const keysArrRed = reduceMapArray(keysArr);
const keysArrUpd = addSpecific(keysArrRed, modelName);
const link = constants.schemas
.indexOf(`${modelName}.[lang].json`) + 1;
addLink(keysArrUpd);
buildStateTree(state, modelName, keysArrUpd, link);
return keysArr;
}
/**
* Create state object from model/form for the summary panel
* @function updateSummaryForm
* @private
* @param {Object} state the state object in JSON
* @param {String} modelName the current model name
* @param {Object} form the form object in JSON
* @param {Object} list hidden advance parameters list
*/
function updateSummaryForm(state, modelName, form) {
let keysArr = [];
// loop trough form keys to create set of keys
$.each(form, key => {
if (key.startsWith('activeForm-')) {
// get all the keys to find the object
let keys = key.split('-').filter(n => n !== '' && n!== 'activeForm');
// remove duplicate keys. They are introduce by array in schema form
const unique = commonService.setUniq(keys);
keysArr.push([unique, form[key].$valid]);
}
});
// Add specific keys so the missing tabs can be represented in the summary
const keysArrUpdate = addSpecific(keysArr, modelName);
const link = constants.schemas
.indexOf(`${modelName}.[lang].json`) + 1;
addLink(keysArr);
buildStateTree(state, modelName, keysArr, link);
}
/**
* Create state object from model/form for the summary panel
* @function buildStateTree
* @private
* @param {Object} state the state object in JSON
* @param {String} element the current element name
* @param {Array} arrKeys array of arrays [[keys], valid: true | false, hlink: 'id']
* @param {Number} mainSection ['map':1 | 'ui':2 | 'services':3 | 'version':4 | 'language':5]
*/
function buildStateTree(state, element, arrKeys, mainSection) {
// SubTab link
let hlink = mainSection;
const firstItems = [];
arrKeys.forEach(arr => firstItems.push(arr[0][0]));
const firstItemsUniq = Array.from(new Set(firstItems));
for (let item of firstItemsUniq) {
const validKey = arrKeys.filter(el => el[0][0] === item).slice();
const maxLen = Math.max(arrKeys.filter(el => el[0][0] === item).length);
// Check if we need to add items attribute
if (maxLen > 1 && item !== 'items') {
// Validity
const validState = isValid(validKey);
// Link
const hlink = uniqId(validKey).replace(/\./g, '-');
state.items
.push({ 'key': item,
'title': '',
items: [],
'valid': validState,
'expand': false,
'masterlink': mainSection,
'hlink': hlink,
'advance': false,
'stype': '',
'shlink': '',
'type': 'object' });
// Build new keys array
const newKeysArr = arrKeys.filter(el => {
if (el[0][0] === item) {
el[0].shift();
return el;
}
});
const index = state.items.findIndex(el => el.key === item);
// Go deeper
buildStateTree(state.items[index], item, newKeysArr, mainSection);
} else if (item !== 'items' && typeof item !== 'undefined') {
// validKey => [[[key], valid, id]]
const valid = validKey[0][1];
const index = constants.schemas.indexOf(`${validKey[0][2]}.[lang].json`);
const hlink = index === -1 ? validKey[0][2].replace(/\./g, '-') : mainSection;
state.items
.push({ 'key': item,
'title': '',
'valid': valid,
'expand': false,
'masterlink': mainSection,
'hlink': hlink,
'advance': false,
'stype': '',
'shlink': '',
'type': 'object' });
}
}
}
/**
* Add hyperlink to arrays of key. The hyperlink === first key of keys array
* @function addLink
* @private
* @param {Array} arrKeys array of arrays [[keys], valid: true | false, hlink: 'id']
*/
function addLink(arrKeys) {
for (let i of arrKeys) {
i.push(i[0][0]);
}
}
/**
* Set the item id in the DOM and return the id
* @function setItemId
* @private
* @param {String} hlink subTab link
* @param {String} elType element type {'baseMaps'|'layers'}
* @param {Number} index rank in elements list
* @return {String} id
*/
function setItemId(hlink, elType, index) {
const el = angular.element(`#${hlink}-pane`);
const children = Array.from(el[0].querySelector("ol").children);
const id = `${elType}-${index}`;
children[index].setAttribute('id', id);
return id;
}
/**
* Return a unique id
* @function uniqId
* @private
* @param {Array} arrKeys array of arrays [[keys], valid: true | false, hlink: 'id']
* @return {String} id
*/
function uniqId(arrKeys) {
const ids = [];
for (let i of arrKeys) ids.push(i[2]);
const id = commonService.setUniq(ids);
if (id.length !== 1) {
console.log('ERROR: MULTIPLE IDS');
}
return id[0];
}
/**
* Return validity
* @function isValid
* @private
* @param {Array} arrKeys array of arrays [[keys], valid: true | false, hlink: 'id']
* @return {Boolean} return false if there's at least one false in validity array
*/
function isValid(arrKeys) {
const valid = [];
for (let i of arrKeys) valid.push(i[1]);
return !valid.includes(false);
}
/**
* Reduce map array to simplify hierarchy
* @function reduceMapArray
* @private
* @param {Array} arrKeys array of object {key: [], valid: true | false}
* @return {Array} reduced keys array
*/
function reduceMapArray(arrKeys) {
const arrLegend = arrKeys.filter(el => el[0][0] === 'legend');
const arrComp = arrKeys.filter(el => el[0][0] === 'components');
const arr = reduceKey(arrKeys, ['tileSchemas'])
.concat(reduceKey(arrKeys, ['extentSets']),
reduceKey(arrKeys, ['lodSets']),
arrComp,
reduceKey(arrKeys, ['baseMaps']),
reduceKey(arrKeys, ['layers']),
arrLegend
);
return arr;
}
/**
* Reduce map array to simplify hierarchy
* @function reduceKey
* @private
* @param {Array} arrKeys array of object {key: [], valid: true | false}
* @param {Array} keys keys
* @return {Array} reduced keys array
*/
function reduceKey(arrKeys, keys) {
const arr = arrKeys.filter(el => el[0].length >= keys.length
&& keys.every((v,i) => v === el[0][i]));
const validSet = [];
for (let i of arr) validSet.push(i[1]);
const valid = !validSet.includes(false);
return [[keys,valid]];
}
/**
* Add specific keys so missing tabs can be part of the summary
* @function addSpecific
* @private
* @param {Array} arrKeys array of object {key: [], valid: true | false}
* @param {String} modelName modelName
* @return {Array} updated keys array
*/
function addSpecific(arrKeys, modelName) {
const keys = {
'map': { 'form.map.extentlods': ['tileSchemas', 'extentSets', 'lodSets'] },
'ui': { 'form.ui.general': ['fullscreen', 'theme', 'legend', 'tableIsOpen', 'failureFeedback'],
'form.ui.nav': ['restrictNavigation', 'navBar'],
'form.ui.sidemenu': ['sideMenu', 'logoUrl', 'title', 'help', 'about']
},
'services': { 'form.service.urls': ['exportMapUrl', 'geometryUrl', 'googleAPIKey', 'proxyUrl'] },
'language': {},
'version': {}
};
arrKeys.forEach(key => {
Object.keys(keys[modelName]).forEach(skey => {
if (keys[modelName][skey].includes(key[0][0])) {
key[0].unshift(skey);
}
});
});
return arrKeys;
}
/**
* Set validity on master element of the model
* @function setMasterValidity
* @private
* @param {Object} state the state object in JSON
*/
function setMasterValidity(state) {
const validSet = [];
for (let i of state.items) validSet.push(i.valid);
state.valid = !validSet.includes(false);
}
}