Skip to content
Draft
18 changes: 18 additions & 0 deletions src/actions/init-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,33 @@ export function isStateless(appDef: ApplicationDefinition) {
}
}

/**
* Represents a loaded resource from the viewer init command.
*
* @since 0.15
*/
export type LoadedResource =
| { kind: 'appdef'; appDef: ApplicationDefinition; sessionOptions: IInitAsyncOptions }
| { kind: 'weblayout'; payload: IInitAppActionPayload };

export interface IViewerInitCommand {
attachClient(client: Client): void;
runAsync(options: IInitAsyncOptions): Promise<IInitAppActionPayload>;
/**
* Builds the {@link IInitAppActionPayload} from a pre-loaded ApplicationDefinition.
* The session (if any) should already be present in `options.session`; if not a new
* session will be created on demand.
*
* @since 0.15
*/
runFromAppDefAsync(appDef: ApplicationDefinition, options: IInitAsyncOptions): Promise<IInitAppActionPayload>;
}

export abstract class ViewerInitCommand<TSubject> implements IViewerInitCommand {
constructor(protected readonly dispatch: ReduxDispatch) { }
public abstract attachClient(client: Client): void;
public abstract runAsync(options: IInitAsyncOptions): Promise<IInitAppActionPayload>;
public abstract runFromAppDefAsync(appDef: ApplicationDefinition, options: IInitAsyncOptions): Promise<IInitAppActionPayload>;
protected abstract isArbitraryCoordSys(map: TSubject): boolean;
protected abstract establishInitialMapNameAndSession(mapsByName: Dictionary<TSubject>): [string, string];
protected abstract setupMaps(appDef: ApplicationDefinition, mapsByName: Dictionary<TSubject>, config: any, warnings: string[], locale: string, pendingMapDefs?: Dictionary<MapToLoad>): Dictionary<MapInfo>;
Expand Down
160 changes: 133 additions & 27 deletions src/actions/init-mapguide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { MgError } from '../api/error';
import { resolveProjectionFromEpsgCodeAsync } from '../api/registry/projections';
import { register } from 'ol/proj/proj4';
import proj4 from "proj4";
import { buildSubjectLayerDefn, getMapDefinitionsFromFlexLayout, isMapDefinition, isStateless, parseMapGroupCoordinateFormat, MapToLoad, ViewerInitCommand } from './init-command';
import { buildSubjectLayerDefn, getMapDefinitionsFromFlexLayout, isMapDefinition, isStateless, parseMapGroupCoordinateFormat, MapToLoad, ViewerInitCommand, LoadedResource } from './init-command';
import { WebLayout } from '../api/contracts/weblayout';
import { convertWebLayoutUIItems, parseCommandsInWebLayout, prepareSubMenus, ToolbarConf } from '../api/registry/command-spec';
import { clearSessionStore, retrieveSelectionSetFromLocalStorage } from '../api/session-store';
Expand Down Expand Up @@ -615,32 +615,86 @@ export class DefaultViewerInitCommand extends ViewerInitCommand<SubjectLayerType
const [mapsByName, pendingMapDefs, warnings] = await this.createRuntimeMapsAsync(session, appDef, isStateless(appDef), fl => getMapDefinitionsFromFlexLayout(fl), fl => this.getExtraProjectionsFromFlexLayout(fl), sessionWasReused);
return await this.initFromAppDefCoreAsync(appDef, this.options, mapsByName, warnings, pendingMapDefs);
}
private async sessionAcquiredAsync(session: AsyncLazy<string>, sessionWasReused: boolean): Promise<IInitAppActionPayload> {
const { resourceId, locale } = this.options;
/**
* Fetches the viewer resource described by `options.resourceId` and returns it
* together with the session context needed for `runFromAppDefAsync`.
*
* - For ApplicationDefinition resources the raw appDef is returned and the
* session (if one was created) is included in `sessionOptions` so that
* `runFromAppDefAsync` can reuse it without opening a second session.
* - For WebLayout resources the full payload is assembled here (including
* optional selection-set recovery) and returned as `{ kind: 'weblayout', payload }`.
*
* @since 0.15
*/
public async loadResourceAsync(options: IInitAsyncOptions): Promise<LoadedResource> {
this.options = options;
await this.initLocaleAsync(options);

const initialSessionWasReused = !!options.session;
// Lazily resolved session string – only evaluated when a MapGuide resource
// actually needs it (createSession API call is deferred until necessary).
let resolvedSession: string | undefined = options.session;
const getSession = async (): Promise<string> => {
if (resolvedSession === undefined) {
assertIsDefined(this.client);
resolvedSession = await this.client.createSession("Anonymous", "");
}
return resolvedSession;
};

// Builds a copy of options carrying the session context for runFromAppDefAsync
const makeSessionOpts = (): IInitAsyncOptions => ({
...options,
session: resolvedSession,
sessionWasReused: initialSessionWasReused
});

const { resourceId, locale } = options;

if (!resourceId) {
//Try assumed default location of appdef.json that we are assuming sits in the same place as the viewer html files
//Try assumed default location of appdef.json
const cl = new Client("", "mapagent");
try {
const fl = await cl.get<ApplicationDefinition>("appdef.json");
return await this.initFromAppDefAsync(fl, session, sessionWasReused);
} catch (e) { //The appdef.json doesn't exist at the assumed default location?
return { kind: 'appdef', appDef: fl, sessionOptions: makeSessionOpts() };
} catch (e) {
throw new MgError(tr("INIT_ERROR_MISSING_RESOURCE_PARAM", locale));
}
} else {
if (typeof (resourceId) == 'string') {
if (strEndsWith(resourceId, "WebLayout")) {
assertIsDefined(this.client);
const wl = await this.client.getResource<WebLayout>(resourceId, { SESSION: await session.getValueAsync() });
return await this.initFromWebLayoutAsync(wl, session, sessionWasReused);
const sessionStr = await getSession();
const sessionLazy = new AsyncLazy<string>(() => Promise.resolve(sessionStr));
const wl = await this.client.getResource<WebLayout>(resourceId, { SESSION: sessionStr });
const payload = await this.initFromWebLayoutAsync(wl, sessionLazy, initialSessionWasReused);
payload.sessionWasReused = initialSessionWasReused;
if (initialSessionWasReused) {
let initSelections: IRestoredSelectionSets = {};
for (const mapName in payload.maps) {
const sset = await retrieveSelectionSetFromLocalStorage(sessionLazy, mapName);
if (sset) {
initSelections[mapName] = sset;
}
}
payload.initialSelections = initSelections;
try {
await clearSessionStore();
} catch (e) {
}
}
return { kind: 'weblayout', payload };
} else if (strEndsWith(resourceId, "ApplicationDefinition")) {
assertIsDefined(this.client);
const fl = await this.client.getResource<ApplicationDefinition>(resourceId, { SESSION: await session.getValueAsync() });
return await this.initFromAppDefAsync(fl, session, sessionWasReused);
const sessionStr = await getSession();
const fl = await this.client.getResource<ApplicationDefinition>(resourceId, { SESSION: sessionStr });
return { kind: 'appdef', appDef: fl, sessionOptions: makeSessionOpts() };
} else {
if (isResourceId(resourceId)) {
throw new MgError(tr("INIT_ERROR_UNKNOWN_RESOURCE_TYPE", locale, { resourceId: resourceId }));
} else {
//Assume URL to a appdef json document
//Assume URL to an appdef json document
let fl: ApplicationDefinition;
if (!this.client) {
// This wasn't set up with a mapagent URI (probably a non-MG viewer template), so make a new client on-the-fly
Expand All @@ -649,32 +703,49 @@ export class DefaultViewerInitCommand extends ViewerInitCommand<SubjectLayerType
} else {
fl = await this.client.get<ApplicationDefinition>(resourceId);
}
return await this.initFromAppDefAsync(fl, session, sessionWasReused);
return { kind: 'appdef', appDef: fl, sessionOptions: makeSessionOpts() };
}
}
} else {
const fl = await resourceId();
return await this.initFromAppDefAsync(fl, session, sessionWasReused);
return { kind: 'appdef', appDef: fl, sessionOptions: makeSessionOpts() };
}
}
}
public async runAsync(options: IInitAsyncOptions): Promise<IInitAppActionPayload> {
/**
* Builds and returns the {@link IInitAppActionPayload} from a pre-loaded
* `appDef`. When `options.session` is present it is reused (avoiding a
* second `createSession` round-trip); otherwise a new session is opened on
* demand (only if the appDef is non-stateless).
*
* `options.sessionWasReused` controls whether selection-set recovery runs.
* When this field is absent it is inferred from the presence of
* `options.session` (matching the behaviour of a direct caller that passes
* an existing session without having gone through `loadResourceAsync`).
*
* @since 0.15
*/
public async runFromAppDefAsync(appDef: ApplicationDefinition, options: IInitAsyncOptions): Promise<IInitAppActionPayload> {
this.options = options;
await this.initLocaleAsync(this.options);
let sessionWasReused = false;
await this.initLocaleAsync(options);

// When sessionWasReused is not explicitly set, infer from whether a session
// was provided by the caller (direct-call path without loadResourceAsync).
const sessionWasReused = options.sessionWasReused ?? !!options.session;
let session: AsyncLazy<string>;
if (!this.options.session) {
if (!options.session) {
session = new AsyncLazy<string>(async () => {
assertIsDefined(this.client);
const sid = await this.client.createSession("Anonymous", "");
return sid;
return this.client.createSession("Anonymous", "");
});
} else {
info(`Re-using session: ${this.options.session}`);
sessionWasReused = true;
session = new AsyncLazy<string>(() => Promise.resolve(this.options.session!));
if (options.sessionWasReused) {
info(`Re-using session: ${options.session}`);
}
session = new AsyncLazy<string>(() => Promise.resolve(options.session!));
}
const payload = await this.sessionAcquiredAsync(session, sessionWasReused);

const payload = await this.initFromAppDefAsync(appDef, session, sessionWasReused);
payload.sessionWasReused = sessionWasReused;
if (sessionWasReused) {
let initSelections: IRestoredSelectionSets = {};
Expand All @@ -686,14 +757,49 @@ export class DefaultViewerInitCommand extends ViewerInitCommand<SubjectLayerType
}
payload.initialSelections = initSelections;
try {
//In the interest of being a responsible citizen, clean up all selection-related stuff from
//session store
await clearSessionStore();
} catch (e) {

}
}

return payload;
}
}
public async runAsync(options: IInitAsyncOptions): Promise<IInitAppActionPayload> {
const resource = await this.loadResourceAsync(options);
if (resource.kind === 'weblayout') {
return resource.payload;
} else {
return this.runFromAppDefAsync(resource.appDef, resource.sessionOptions);
}
}
}

/**
* Loads the viewer resource (ApplicationDefinition or WebLayout) from the location described
* by `options`. This is a standalone non-dispatchable async function; callers obtain the
* loaded resource and then decide what action to dispatch (typically `initAppFromAppDef`
* for ApplicationDefinition resources).
*
* @param {DefaultViewerInitCommand} cmd - A concrete viewer init command instance. For
* MapGuide resources (ApplicationDefinition or WebLayout resource IDs), the client must
* already be attached via `cmd.attachClient(client)` before calling this function.
* @param {IInitAsyncOptions} options
* @returns {Promise<LoadedResource>}
*
* @example
* ```typescript
* // Attach the client for MapGuide resources, then load the resource and dispatch init:
* if (agentUri && agentKind) {
* initCommand.attachClient(new Client(agentUri, agentKind));
* }
* const resource = await loadViewerResourceAsync(initCommand, opts);
* if (resource.kind === 'appdef') {
* dispatch(initAppFromAppDef(initCommand, resource.sessionOptions, resource.appDef, viewer));
* }
* ```
*
* @since 0.15
*/
export function loadViewerResourceAsync(cmd: DefaultViewerInitCommand, options: IInitAsyncOptions): Promise<LoadedResource> {
return cmd.loadResourceAsync(options);
}
102 changes: 62 additions & 40 deletions src/actions/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function applyInitialBaseLayerVisibility(externalBaseLayers: IExternalBas
}
}

function processAndDispatchInitError(error: Error, includeStack: boolean, dispatch: ReduxDispatch, opts: IInitAsyncOptions): void {
export function processAndDispatchInitError(error: Error, includeStack: boolean, dispatch: ReduxDispatch, opts: IInitAsyncOptions): void {
if (error.stack) {
dispatch({
type: ActionType.INIT_ERROR,
Expand Down Expand Up @@ -137,6 +137,14 @@ export function normalizeInitPayload(payload: IInitAppActionPayload, layout: str

export interface IInitAsyncOptions extends IInitAppLayout {
locale: string;
/**
* When set, indicates whether the session in `session` was a pre-existing session
* that the caller is reusing (true) or a freshly-created session (false). Used to
* control whether selection-set recovery should run.
*
* @since 0.15
*/
sessionWasReused?: boolean;
}

let _counter = 0;
Expand Down Expand Up @@ -295,61 +303,75 @@ export function processLayerInMapGroup(map: MapConfiguration, warnings: string[]
}

/**
* Initializes the viewer
* Applies viewer-mount overrides and appSettings merging onto an already-built
* {@link IInitAppActionPayload}. Mutates `payload` in place.
*
* @hidden
* @since 0.15
*/
export function applyInitPayloadOverrides(payload: IInitAppActionPayload, opts: IInitAsyncOptions): void {
if (opts.initialView) {
payload.initialView = { ...opts.initialView };
}
if (opts.initialActiveMap) {
payload.activeMapName = opts.initialActiveMap;
}
payload.initialHideGroups = opts.initialHideGroups;
payload.initialHideLayers = opts.initialHideLayers;
payload.initialShowGroups = opts.initialShowGroups;
payload.initialShowLayers = opts.initialShowLayers;
payload.featureTooltipsEnabled = opts.featureTooltipsEnabled;
// Merge in appSettings from loaded appDef. Any setting already specified at
// viewer mount will be overwritten by the appDef value.
const appSettings = opts.appSettings ?? {};
const inAppSettings = payload.appSettings ?? {};
for (const k in inAppSettings) {
appSettings[k] = inAppSettings[k];
}
payload.appSettings = appSettings;
}

/**
* Thunked action that initialises the viewer from a pre-loaded
* {@link ApplicationDefinition}.
*
* Given an already-fetched `appDef`, `cmd`, `options`, and `viewer`, this action
* assembles the full {@link IInitAppActionPayload} (handling locale, session management,
* runtime-map creation, toolbar/widget parsing, etc.) and dispatches `INIT_APP`.
*
* This is the canonical initialisation path for ApplicationDefinition-based viewers.
* Callers that hold a prepared `appDef` (e.g. after calling
* {@link IViewerInitCommand.loadResourceAsync}) can dispatch this action directly,
* enabling deterministic testing of the full init payload without needing to exercise
* the fetch infrastructure.
*
* @param {IViewerInitCommand} cmd
* @param {IMapProviderContext} viewer
* @param {IInitAppLayout} options
* @param {ApplicationDefinition} appDef
* @param {IMapProviderContext} viewer
* @returns {ReduxThunkedAction}
*
* @since 0.15 Added viewer parameter
*
* @since 0.15
*/
export function initLayout(cmd: IViewerInitCommand, viewer: IMapProviderContext, options: IInitAppLayout): ReduxThunkedAction {
const opts: IInitAsyncOptions = { ...options };
export function initAppFromAppDef(cmd: IViewerInitCommand, options: IInitAppLayout, appDef: ApplicationDefinition, viewer: IMapProviderContext): ReduxThunkedAction {
return (dispatch, getState) => {
const args = getState().config;
//TODO: Fetch and init the string bundle earlier if "locale" is present
//so the English init messages are seen only for a blink if requesting a
//non-english string bundle
const opts: IInitAsyncOptions = { ...options };
if (args.agentUri && args.agentKind) {
const client = new Client(args.agentUri, args.agentKind);
cmd.attachClient(client);
cmd.attachClient(new Client(args.agentUri, args.agentKind));
}
cmd.runAsync(options).then(payload => {
let initPayload = payload;
if (opts.initialView) {
initPayload.initialView = {
...opts.initialView
};
}
if (opts.initialActiveMap) {
initPayload.activeMapName = opts.initialActiveMap;
}
initPayload.initialHideGroups = opts.initialHideGroups;
initPayload.initialHideLayers = opts.initialHideLayers;
initPayload.initialShowGroups = opts.initialShowGroups;
initPayload.initialShowLayers = opts.initialShowLayers;
initPayload.featureTooltipsEnabled = opts.featureTooltipsEnabled;
// Merge in appSettings from loaded appDef, any setting in appDef
// already specified at viewer mount will be overwritten
const appSettings = opts.appSettings ?? {};
const inAppSettings = payload.appSettings ?? {};
for (const k in inAppSettings) {
appSettings[k] = inAppSettings[k];
}
initPayload.appSettings = appSettings;
return cmd.runFromAppDefAsync(appDef, opts).then(payload => {
applyInitPayloadOverrides(payload, opts);
dispatch({
type: ActionType.INIT_APP,
payload
});
if (options.onInit) {
if (viewer) {
options.onInit(viewer);
}
if (opts.onInit && viewer) {
opts.onInit(viewer);
}
}).catch(err => {
processAndDispatchInitError(err, false, dispatch, opts);
})
});
};
}

Expand Down
Loading
Loading