@ngageoint/mage.arcgis.service
Version:
A mage service plugin that synchronizes mage observations to a configured ArcGIS feature layer.
309 lines • 16.7 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ObservationProcessor = void 0;
const ArcGISPluginConfig_1 = require("./types/ArcGISPluginConfig");
const ObservationsTransformer_1 = require("./ObservationsTransformer");
const ArcObjects_1 = require("./ArcObjects");
const FeatureService_1 = require("./FeatureService");
const LayerInfo_1 = require("./LayerInfo");
const FeatureLayerProcessor_1 = require("./FeatureLayerProcessor");
const EventTransform_1 = require("./EventTransform");
const GeometryChangedHandler_1 = require("./GeometryChangedHandler");
const EventDeletionHandler_1 = require("./EventDeletionHandler");
const EventLayerProcessorOrganizer_1 = require("./EventLayerProcessorOrganizer");
const FeatureServiceAdmin_1 = require("./FeatureServiceAdmin");
/**
* Class that wakes up at a certain configured interval and processes any new observations that can be
* sent to any specified ArcGIS feature layers.
*/
class ObservationProcessor {
/**
* Constructor.
* @param {PluginStateRepository<ArcGISPluginConfig>} stateRepo The plugins configuration.
* @param {MageEventRepository} eventRepo Used to get all the active events.
* @param {ObservationRepositoryForEvent} obsRepos Used to get new observations.
* @param {UserRepository} userRepo Used to get user information.
* @param {ArcGISIdentityService} identityService Used to manager ArcGIS user identities.
* @param {Console} console Used to log to the console.
*/
constructor(stateRepo, eventRepo, obsRepos, userRepo, identityService, console) {
/**
* True if the processor is currently active, false otherwise.
*/
this._isRunning = false;
/**
* Sends observations to a single feature layer.
*/
this._layerProcessors = [];
/**
* True if this is a first run at updating arc feature layers. If so we need to make sure the layers are
* all up to date.
*/
this._firstRun = true;
this._stateRepo = stateRepo;
this._eventRepo = eventRepo;
this._obsRepos = obsRepos;
this._userRepo = userRepo;
this._identityService = identityService;
this._console = console;
this._organizer = new EventLayerProcessorOrganizer_1.EventLayerProcessorOrganizer();
this._transformer = new ObservationsTransformer_1.ObservationsTransformer(ArcGISPluginConfig_1.defaultArcGISPluginConfig, console);
this._geometryChangeHandler = new GeometryChangedHandler_1.GeometryChangedHandler(this._transformer);
this._eventDeletionHandler = new EventDeletionHandler_1.EventDeletionHandler(this._console, ArcGISPluginConfig_1.defaultArcGISPluginConfig);
}
/**
* Gets the current configuration from the database.
* @returns {Promise<ArcGISPluginConfig>} The current configuration from the database.
*/
safeGetConfig() {
return __awaiter(this, void 0, void 0, function* () {
const state = yield this._stateRepo.get();
if (!state)
return yield this._stateRepo.put(ArcGISPluginConfig_1.defaultArcGISPluginConfig);
return yield this._stateRepo.get().then((state) => state ? state : this._stateRepo.put(ArcGISPluginConfig_1.defaultArcGISPluginConfig));
});
}
/**
* Puts a new confguration in the state repo.
* @param {ArcGISPluginConfig} newConfig The new config to put into the state repo.
* @returns {Promise<ArcGISPluginConfig>} The updated configuration.
*/
putConfig(newConfig) {
return __awaiter(this, void 0, void 0, function* () {
return yield this._stateRepo.put(newConfig);
});
}
/**
* Updates the confguration in the state repo.
* @param {ArcGISPluginConfig} newConfig The new config to put into the state repo.
* @returns {Promise<ArcGISPluginConfig>} The updated configuration.
*/
patchConfig(newConfig) {
return __awaiter(this, void 0, void 0, function* () {
return yield this._stateRepo.patch(newConfig);
});
}
/**
* Gets the current configuration and updates the processor if needed
* @returns {Promise<ArcGISPluginConfig>} The current configuration from the database.
*/
updateConfig() {
return __awaiter(this, void 0, void 0, function* () {
const config = yield this.safeGetConfig();
if (!config.enabled) {
this._console.info('ArcGIS plugin is disabled, stopping processor');
this.stop();
return config;
}
// Include configured eventform definitions while detecting changes in config
const eventIds = config.featureServices
.flatMap(service => service.layers)
.flatMap(layer => layer.eventIds)
.filter((eventId) => typeof eventId === 'number');
const eventForms = yield this._eventRepo.findAllByIds(eventIds);
const fullConfig = Object.assign(Object.assign({}, config), { eventForms });
const configJson = JSON.stringify(fullConfig);
if (this._previousConfig == null || this._previousConfig !== configJson) {
this._transformer = new ObservationsTransformer_1.ObservationsTransformer(config, console);
this._geometryChangeHandler = new GeometryChangedHandler_1.GeometryChangedHandler(this._transformer);
this._eventDeletionHandler.updateConfig(config);
this._layerProcessors = [];
this._previousConfig = configJson;
yield this.getFeatureServiceLayers(config);
}
return config;
});
}
/**
* Starts the processor.
*/
start() {
return __awaiter(this, void 0, void 0, function* () {
this._isRunning = true;
this._firstRun = true;
yield this.processAndScheduleNext();
});
}
/**
* Stops the processor.
*/
stop() {
this._isRunning = false;
clearTimeout(this._nextTimeout);
}
/**
* Gets information on all the configured features service layers.
* @param {ArcGISPluginConfig} config The plugins configuration.
*/
getFeatureServiceLayers(config) {
return __awaiter(this, void 0, void 0, function* () {
const promises = [];
for (const service of config.featureServices) {
promises.push((() => __awaiter(this, void 0, void 0, function* () {
try {
this._console.info(`Getting feature service layers for ${service.url}`);
const identityManager = yield this._identityService.signin(service);
const featureService = new FeatureService_1.FeatureService(console, service, identityManager);
const arcService = yield featureService.getService();
for (const featureLayer of arcService.layers) {
const featureLayerConfig = service.layers.find(layer => layer.layer.toString() === featureLayer.name.toString());
if (featureLayerConfig) {
const url = `${service.url}/${featureLayer.id}`;
const layerInfo = yield featureService.getLayer(featureLayer.id);
if (featureLayer.geometryType != null) {
// TODO The featureLayerConfig should contain the layer id
featureLayerConfig.layer = featureLayer.id;
const admin = new FeatureServiceAdmin_1.FeatureServiceAdmin(config, this._identityService, this._console);
const eventIds = featureLayerConfig.eventIds || [];
const layerFields = yield admin.updateLayer(service, featureLayerConfig, layerInfo, this._eventRepo);
const info = new LayerInfo_1.LayerInfo(url, eventIds, Object.assign(Object.assign({}, layerInfo), { fields: layerFields }));
const layerProcessor = new FeatureLayerProcessor_1.FeatureLayerProcessor(info, config, identityManager, this._console);
this._layerProcessors.push(layerProcessor);
}
}
}
}
catch (err) {
this._console.error(`Error getting feature service layers for ${service.url}:`, err);
}
}))());
}
yield Promise.all(promises);
});
}
/**
* Processes any new observations and then schedules its next run if it hasn't been stopped.
*/
processAndScheduleNext() {
return __awaiter(this, void 0, void 0, function* () {
const config = yield this.updateConfig();
if (this._isRunning) {
if (config.enabled && this._layerProcessors.length > 0) {
this._console.info('ArcGIS plugin checking for any pending updates or adds');
const pendingPromises = [];
for (const layerProcessor of this._layerProcessors) {
pendingPromises.push(layerProcessor.processPendingUpdates());
}
yield Promise.all(pendingPromises);
this._console.info('ArcGIS plugin processing new observations...');
const enabledEvents = (yield this._eventRepo.findActiveEvents()).filter(event => this._layerProcessors.some(layerProcessor => layerProcessor.layerInfo.hasEvent(event.id)));
this._eventDeletionHandler.checkForEventDeletion(enabledEvents, this._layerProcessors, this._firstRun);
const eventsToProcessors = this._organizer.organize(enabledEvents, this._layerProcessors);
const nextQueryTime = Date.now();
const promises = [];
for (const pair of eventsToProcessors) {
promises.push(new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
this._console.info('ArcGIS getting newest observations for event ' + pair.event.name);
const obsRepo = yield this._obsRepos(pair.event.id);
const pagingSettings = {
pageSize: config.batchSize,
pageIndex: 0,
includeTotalCount: true
};
let morePages = true;
let numberLeft = 0;
while (morePages) {
numberLeft = yield this.queryAndSend(config, pair.featureLayerProcessors, obsRepo, pagingSettings, numberLeft);
morePages = numberLeft > 0;
}
resolve();
})));
}
yield Promise.all(promises);
for (const layerProcessor of this._layerProcessors) {
layerProcessor.lastTimeStamp = nextQueryTime;
}
this._firstRun = false;
// ArcGISIndentityManager access tokens may have been updated check and save
this._identityService.updateIndentityManagers();
}
this.scheduleNext(config);
}
});
}
scheduleNext(config) {
if (this._isRunning) {
let interval = config.intervalSeconds;
if (this._firstRun && config.featureServices.length > 0) {
interval = config.startupIntervalSeconds;
}
else {
for (const layerProcessor of this._layerProcessors) {
if (layerProcessor.hasPendingUpdates()) {
interval = config.updateIntervalSeconds;
break;
}
}
}
this._nextTimeout = setTimeout(() => { this.processAndScheduleNext(); }, interval * 1000);
}
}
/**
* Queries for new observations and sends them to any configured arc servers.
* @param {ArcGISPluginConfig} config The plugin configuration.
* @param {FeatureLayerProcessor[]} layerProcessors The layer processors to use when processing arc objects.
* @param {EventScopedObservationRepository} obsRepo The observation repo for an event.
* @param {PagingParameters} pagingSettings Current paging settings.
* @param {number} numberLeft The number of observations left to query and send to arc.
* @returns {Promise<number>} The number of observations still needing to be queried and sent to arc.
*/
queryAndSend(config, layerProcessors, obsRepo, pagingSettings, numberLeft) {
return __awaiter(this, void 0, void 0, function* () {
let newNumberLeft = numberLeft;
let queryTime = -1;
for (const layerProcessor of layerProcessors) {
if (queryTime === -1 || layerProcessor.lastTimeStamp < queryTime) {
queryTime = layerProcessor.lastTimeStamp;
}
}
const latestObs = yield obsRepo.findLastModifiedAfter(queryTime, pagingSettings);
if ((latestObs === null || latestObs === void 0 ? void 0 : latestObs.totalCount) != null && latestObs.totalCount > 0) {
if (pagingSettings.pageIndex === 0) {
this._console.info('ArcGIS newest observation count ' + latestObs.totalCount);
newNumberLeft = latestObs.totalCount;
}
const observations = latestObs.items;
const mageEvent = yield this._eventRepo.findById(obsRepo.eventScope);
const eventTransform = new EventTransform_1.EventTransform(config, mageEvent);
const arcObjects = new ArcObjects_1.ArcObjects();
this._geometryChangeHandler.checkForGeometryChange(observations, arcObjects, layerProcessors, this._firstRun);
for (const observation of observations) {
// TODO: Should archived observations be removed after a certain time? Also this uses 'startsWith' because not all deleted observations use 'archived' which is a bug
if (observation.states.length > 0 && observation.states[0].name.startsWith('archive')) {
const arcObservation = this._transformer.createObservation(observation);
arcObjects.deletions.push(arcObservation);
}
else {
let user = null;
if (observation.userId != null) {
user = yield this._userRepo.findById(observation.userId);
}
const arcObservation = this._transformer.transform(observation, eventTransform, user);
arcObjects.add(arcObservation);
}
}
arcObjects.firstRun = this._firstRun;
for (const layerProcessor of layerProcessors) {
layerProcessor.processArcObjects(arcObjects);
}
newNumberLeft -= latestObs.items.length;
pagingSettings.pageIndex++;
}
else {
this._console.info('ArcGIS no new observations');
}
return newNumberLeft;
});
}
}
exports.ObservationProcessor = ObservationProcessor;
//# sourceMappingURL=ObservationProcessor.js.map