Skip to content

Commit

Permalink
Get config from url (#2091)
Browse files Browse the repository at this point in the history
* Get config from url

* Completed the sandbox implementation
  • Loading branch information
ychoquet authored May 6, 2024
1 parent ba9c807 commit 15af70d
Show file tree
Hide file tree
Showing 12 changed files with 411 additions and 21 deletions.
69 changes: 64 additions & 5 deletions packages/geoview-core/public/templates/config-sandbox.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
background: #282a3a;
border-radius: 2px;
padding: 20px 10px;
height: 600px;
height: 500px;
overflow-y: auto;
}

Expand Down Expand Up @@ -92,7 +92,7 @@ <h1><strong>Sandbox Configuration</strong></h1>
<div class="line-numbers" id="inputLineNumbers">
<span></span>
</div>
<textarea id="configGeoview" name="configuration" rows="30" cols="110">
<textarea id="configGeoview" name="configuration" cols="110">
{
'map': {
'interaction': 'dynamic',
Expand Down Expand Up @@ -139,11 +139,12 @@ <h1><strong>Sandbox Configuration</strong></h1>
</div>
</td>
<td>
<span id="validationMessage" style="margin:10px;">File not validated...</span>
<div class="editor">
<div class="line-numbers" id="outputLineNumbers">
<span></span>
</div>
<textarea id="configOutput" name="configuration" rows="30" cols="110">
<textarea id="configOutput" name="configuration" cols="110">
</textarea>
</div>
</td>
Expand All @@ -160,7 +161,24 @@ <h1><strong>Sandbox Configuration</strong></h1>
<button id="createMap" style="margin:10px;">Create Map</button>
<button id="deleteMap" style="margin:10px;">Delete map</button>
<br />
<span id="validationMessage" style="margin:10px;">File not validated...</span>
</div>
<div class="editor" style="height:60px">
<div class="line-numbers" id="inputLineNumbers">
<span></span>
</div>
<div>
<textarea id="configUrlGeoview" name="Urlconfiguration" cols="210">
p=3857&z=4&c=-100,40&l=en&t=dark&b={basemapId:transport,shaded:false,labeled:true}&i=dynamic&cc=overview-map&keys=12acd145-626a-49eb-b850-0a59c9bc7506,ccc75c12-5acc-4a6a-959f-ef6f621147b9
</textarea>
</div>
</div>
<div>
<button id="validateUrlConfig" style="margin:10px;">Validate</button>
<select id="urlLanguage">
<option value="en">English</option>
<option value="fr">Français</option>
</select>
<br />
</div>
<div class="map-title-holder">
<h4 id="HLCONF1">Sanbox Map</h4>
Expand All @@ -181,8 +199,10 @@ <h4 id="HLCONF1">Sanbox Map</h4>
const isValid = false;
const configAreaString = document.getElementById('configGeoview').value.replaceAll(' ', '');
document.getElementById('configGeoview').value = configAreaString;
const configUrlAreaString = document.getElementById('configUrlGeoview').value.replaceAll(' ', '');
document.getElementById('configUrlGeoview').value = configUrlAreaString;

// Validate Button============================================================================================================
// Config Validate Button============================================================================================================
const validateJSONButton = document.getElementById('validateConfig');

// add an event listener when a button is clicked
Expand Down Expand Up @@ -217,6 +237,45 @@ <h4 id="HLCONF1">Sanbox Map</h4>
}
});

// Config Url Validate Button========================================================================================================
const validateURLButton = document.getElementById('validateUrlConfig');

// add an event listener when the button is clicked
validateURLButton.addEventListener('click', function (e) {
// get message element
const message = document.getElementById('validationMessage');
const langue = document.getElementById('urlLanguage').value;
const configArea = document.getElementById('configGeoview');
const configUrlArea = document.getElementById('configUrlGeoview');
const configOutput = document.getElementById('configOutput');

configArea.value = configUrlArea.value;
// get config and test if URL Config is valid
const returnedValue = cgpv.api.configApi.getConfigFromUrl(configUrlArea.value);
returnedValue.then((mapConfig) => {
configOutput.value = mapConfig.getIndentedJsonString();

// Generate line numbers
(() => {
const textarea = document.getElementById('configOutput');
const lineNumbersContainer = document.getElementById('outputLineNumbers');
const lines = textarea.value.split('\n').length;
const lineNumbers = Array.from({ length: lines }, (_, index) => '').join('<span />');
lineNumbersContainer.innerHTML = lineNumbers;
})();

// set class and message
message.classList.add('config-json-valid');
message.classList.remove('config-error');
if (mapConfig.isValid) {
message.innerHTML = 'File is valid, see console for details...';
document.getElementById('createMap').disabled = true;
} else {
message.innerHTML = 'File is invalid, see console for details...';
}
});
});

// Create Button============================================================================================================
const createMapButton = document.getElementById('createMap');
createMapButton.disabled = true;
Expand Down
158 changes: 156 additions & 2 deletions packages/geoview-core/src/api/config/config-api.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { CV_DEFAULT_MAP_FEATURE_CONFIG } from '@config/types/config-constants';
import { TypeJsonObject, toJsonObject } from '@config/types/config-types';
import { Cast, TypeJsonValue, TypeJsonObject, toJsonObject, TypeJsonArray } from '@config/types/config-types';
import { TypeDisplayLanguage } from '@config/types/map-schema-types';
import { MapFeatureConfig } from '@config/types/classes/map-feature-config';
import { UUIDmapConfigReader } from '@config/uuid-config-reader';
import { logger } from '@/core//utils/logger';

/**
* The API class that create configuration object. It is used to validate and read the service and layer metadata.
Expand All @@ -10,12 +12,164 @@ import { MapFeatureConfig } from '@config/types/classes/map-feature-config';
*/
export class ConfigApi {
/**
* @static
* Parse the parameters obtained from a url.
*
* @param {string} urlParams The parameters found on the url after the ?.
*
* @returns {TypeJsonObject} Object containing the parsed params.
* @static @private
*/
static #getMapPropsFromUrlParams(urlParams: string): TypeJsonObject {
// Get parameters from path. Ex: x=123&y=456 will get {"x": 123, "z": "456"}
const obj: TypeJsonObject = {};

if (urlParams !== undefined) {
const params = urlParams.split('&');

for (let i = 0; i < params.length; i += 1) {
const param = params[i].split('=');
const key = param[0];
const value = param[1] as TypeJsonValue;

obj[key] = Cast<TypeJsonObject>(value);
}
}

return obj;
}

/**
* Get url parameters from url param search string.
*
* @param {objStr} objStr the url parameter string.
*
* @returns {TypeJsonObject} an object containing url parameters.
* @staric @private
*/
static #parseObjectFromUrl(objStr: string): TypeJsonObject {
const obj: TypeJsonObject = {};

if (objStr && objStr.length) {
// get the text in between { }
const objStrPropRegex = /(?:[{_.])(.*?)(?=[}_.])/g;

const objStrProps = objStr.match(objStrPropRegex);

if (objStrProps && objStrProps.length) {
// first { is kept with regex, remove
const objProps = objStrProps[0].replace(/{/g, '').split(',');

if (objProps) {
for (let i = 0; i < objProps.length; i += 1) {
const prop = objProps[i].split(':');
if (prop && prop.length) {
const key: string = prop[0];
const value: string = prop[1];

if (prop[1] === 'true') {
obj[key] = Cast<TypeJsonObject>(true);
} else if (prop[1] === 'false') {
obj[key] = Cast<TypeJsonObject>(false);
} else {
obj[key] = Cast<TypeJsonObject>(value);
}
}
}
}
}
}

return obj;
}

/**
* Get a map feature config from url parameters.
* @param {string} urlStringParams The url parameters.
*
* @returns {Promise<MapFeatureConfig | undefined>} A map feature configuration object generated from url parameters.
* @static @async
*/
static async getConfigFromUrl(urlStringParams: string): Promise<MapFeatureConfig | undefined> {
// return the parameters as an object if url contains any params
const urlParams = ConfigApi.#getMapPropsFromUrlParams(urlStringParams);

// if user provided any url parameters update
const jsonConfig = {} as TypeJsonObject;

// update the language if provided from the map configuration.
const displayLanguage = (urlParams.l as TypeDisplayLanguage) || 'en';

if (Object.keys(urlParams).length && !urlParams.geoms) {
// Ex: p=3857&z=4&c=40,-100&l=en&t=dark&b={basemapId:transport,shaded:false,labeled:true}&i=dynamic&cp=details-panel,layers-panel&cc=overview-map&keys=12acd145-626a-49eb-b850-0a59c9bc7506,ccc75c12-5acc-4a6a-959f-ef6f621147b9

// get center
let center: string[] = [];
if (urlParams.c) center = (urlParams.c as string).split(',');
if (center.length !== 2)
center = [
CV_DEFAULT_MAP_FEATURE_CONFIG.map.viewSettings.initialView!.zoomAndCenter![1][0]!.toString(),
CV_DEFAULT_MAP_FEATURE_CONFIG.map.viewSettings.initialView!.zoomAndCenter![1][1].toString(),
];

// get zoom
let zoom = CV_DEFAULT_MAP_FEATURE_CONFIG.map.viewSettings.initialView!.zoomAndCenter![0].toString();
if (urlParams.z) zoom = urlParams.z as string;

jsonConfig.map = {
interaction: urlParams.i as TypeJsonObject,
viewSettings: {
initialView: {
zoomAndCenter: [parseInt(zoom, 10), [parseInt(center[0], 10), parseInt(center[1], 10)]] as TypeJsonObject,
},
projection: parseInt(urlParams.p as string, 10) as TypeJsonObject,
},
basemapOptions: ConfigApi.#parseObjectFromUrl(urlParams.b as string),
listOfGeoviewLayerConfig: Cast<TypeJsonObject>([]),
};

// get layer information from catalog using their uuid's if any passed from url params
if (urlParams.keys) {
try {
// Get the layers config
const promise = UUIDmapConfigReader.getGVConfigFromUUIDs(
CV_DEFAULT_MAP_FEATURE_CONFIG.serviceUrls.geocoreUrl,
displayLanguage.split('-')[0],
urlParams.keys.toString().split(',')
);
(jsonConfig.map.listOfGeoviewLayerConfig as TypeJsonObject[]) = await promise;
} catch (error) {
// Log
logger.logError('Failed to get the GeoView layers from url keys', urlParams.keys, error);
}
}

// get core components
if (urlParams.cc) {
(jsonConfig.components as TypeJsonArray) = (urlParams.cc as string).split(',') as TypeJsonArray;
}

// get core packages if any
if (urlParams.cp) {
(jsonConfig.corePackages as TypeJsonArray) = (urlParams.cp as string).split(',') as TypeJsonArray;
}

// update the version if provided from the map configuration.
jsonConfig.schemaVersionUsed = urlParams.v as TypeJsonObject;
}

// Trace the detail config read from url
logger.logTraceDetailed('URL Config - ', jsonConfig);

return new MapFeatureConfig(jsonConfig, displayLanguage);
}

/**
* Get the default values that are applied to the map feature configuration when the user doesn't provide a value for a field
* that is covered by a default value.
* @param {TypeDisplayLanguage} language The language of the map feature config we want to produce.
*
* @returns {MapFeatureConfig} The map feature configuration default values.
* @static
*/
static getDefaultMapFeatureConfig(language: TypeDisplayLanguage): MapFeatureConfig {
return new MapFeatureConfig(toJsonObject(CV_DEFAULT_MAP_FEATURE_CONFIG), language);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ export abstract class AbstractGeoviewLayerConfig {
/** The GeoView layer identifier. */
geoviewLayerId: string;

/** Type of GeoView layer. */
abstract geoviewLayerType: TypeGeoviewLayerType;

/**
* The display name of the layer (English/French). If it is not present the viewer will make an attempt to scrape this
* information.
Expand Down Expand Up @@ -97,18 +94,21 @@ export abstract class AbstractGeoviewLayerConfig {
.filter((subLayerConfig) => {
return subLayerConfig;
}) as ConfigBaseClass[];
this.#validate();
}

/**
* Validate the object properties. Layer name and type must be set.
* @private
*/
#validate(): void {
if (!this.geoviewLayerName)
protected validate(): void {
if (!this.geoviewLayerName) {
logger.logError(`Property geoviewLayerName is mandatory for GeoView layer ${this.geoviewLayerId} of type ${this.geoviewLayerType}.`);
if (!this.geoviewLayerType)
this.propagateError();
}
if (!this.geoviewLayerType) {
logger.logError(`Property geoviewLayerType is mandatory for GeoView layer ${this.geoviewLayerId} of type ${this.geoviewLayerType}.`);
this.propagateError();
}
}

/**
Expand All @@ -126,6 +126,14 @@ export abstract class AbstractGeoviewLayerConfig {
*/
protected abstract get geoviewLayerSchema(): string;

/**
* The getter method that returns the geoview layer type to use for the validation.
*
* @returns {string} The GeoView layer schema associated to the config.
* @protected @abstract
*/
abstract get geoviewLayerType(): TypeGeoviewLayerType;

/**
* The method used to implement the class factory model that returns the instance of the class based on the sublayer
* type needed.
Expand Down Expand Up @@ -155,4 +163,13 @@ export abstract class AbstractGeoviewLayerConfig {
this.#errorDetected = true;
this.#mapFeatureConfig?.propagateError();
}

/**
* The getter method that returns the isValid flag (true when the map feature config is valid).
*
* @returns {boolean} The isValid property associated to map feature config.
*/
get isValid(): boolean {
return !this.#errorDetected;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export class EsriDynamicLayerConfig extends AbstractGeoviewLayerConfig {
if (!this.metadataAccessPath) {
throw new Error(`metadataAccessPath is mandatory for GeoView layer ${this.geoviewLayerId} of type ${this.geoviewLayerType}.`);
}
this.validate();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export class EsriFeatureLayerConfig extends AbstractGeoviewLayerConfig {
if (!this.metadataAccessPath) {
throw new Error(`metadataAccessPath is mandatory for GeoView layer ${this.geoviewLayerId} of type ${this.geoviewLayerType}.`);
}
this.validate();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class MapFeatureConfig {
this.theme = (clonedJsonConfig.theme || CV_DEFAULT_MAP_FEATURE_CONFIG.theme) as TypeDisplayTheme;
this.navBar = [...((clonedJsonConfig.navBar || CV_DEFAULT_MAP_FEATURE_CONFIG.navBar) as TypeNavBarProps)];
this.appBar = Cast<TypeAppBarProps>(defaultsDeep(clonedJsonConfig.appBar, CV_DEFAULT_MAP_FEATURE_CONFIG.appBar));
this.footerBar = Cast<TypeFooterBarProps>(defaultsDeep(clonedJsonConfig.footerBar, CV_DEFAULT_MAP_FEATURE_CONFIG.footerBar));
this.footerBar = Cast<TypeFooterBarProps>(clonedJsonConfig.footerBar);
this.overviewMap = Cast<TypeOverviewMapProps>(defaultsDeep(clonedJsonConfig.overviewMap, CV_DEFAULT_MAP_FEATURE_CONFIG.overviewMap));
this.components = [...((clonedJsonConfig.components || CV_DEFAULT_MAP_FEATURE_CONFIG.components) as TypeMapComponents)];
this.corePackages = [...((clonedJsonConfig.corePackages || CV_DEFAULT_MAP_FEATURE_CONFIG.corePackages) as TypeMapCorePackages)];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export abstract class AbstractBaseLayerEntryConfig extends ConfigBaseClass {
initialSettings: TypeLayerInitialSettings,
language: TypeDisplayLanguage,
geoviewLayerConfig: AbstractGeoviewLayerConfig,
parentNode: ConfigBaseClass
parentNode?: ConfigBaseClass
) {
super(layerConfig, initialSettings, language, geoviewLayerConfig, parentNode);
// If the user has provided a source then keep it, else create an empty one.
Expand Down
Loading

0 comments on commit 15af70d

Please sign in to comment.