adjuster.service.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. /* eslint-disable no-restricted-syntax */
  2. /* eslint-disable no-console */
  3. import {HttpClient} from '@angular/common/http';
  4. import {Injectable} from '@angular/core';
  5. import {forkJoin} from 'rxjs';
  6. import {HsDialogContainerService} from 'hslayers-ng';
  7. import {HsToastService} from 'hslayers-ng';
  8. import {HsUtilsService} from 'hslayers-ng';
  9. import attractivenessConfig from '../attractiveness.config.json';
  10. import clusteringMethods from '../data/clustering_methods.json';
  11. import {AdjusterEventService} from './adjuster-event.service';
  12. import {AdjusterLegendService} from './adjuster-legend.service';
  13. import {AdjusterPresetsService, Factor} from './adjuster-presets.service';
  14. import {
  15. AttractivenessClustersService,
  16. MethodDescription,
  17. } from './attractiveness-clusters.service';
  18. import {AttractivenessIndexService} from './attractiveness-index.service';
  19. import {RDFSubject} from './ontology.model';
  20. import {nuts3IndexSource} from '../app.config';
  21. @Injectable({providedIn: 'root'})
  22. export class AdjusterService {
  23. /** To be read from a config file */
  24. allowedClusteringMethods: Array<string>;
  25. /** To be read from a config file */
  26. initialWeights: Record<string, number>;
  27. /** To be read from a config file */
  28. serviceBaseUrl: string;
  29. /** Used in the UI as a selector */
  30. allowClusters = true;
  31. /** Used in the UI as a selector */
  32. allowIndex = false;
  33. factors: Array<Factor> = [];
  34. layersReady = new Set();
  35. //clusters = [];
  36. //method: string;
  37. methods: Array<MethodDescription>;
  38. numberOfClusters: number;
  39. private _clusteringInProcess: boolean;
  40. private _clustersLoaded: boolean;
  41. /** Once instantiated, the load is definitely in process */
  42. private _loadInProcess = true;
  43. private _raiInProcess: boolean;
  44. constructor(
  45. public adjusterEventService: AdjusterEventService,
  46. public adjusterLegendService: AdjusterLegendService,
  47. public adjusterPresetsService: AdjusterPresetsService,
  48. public attractivenessIndexService: AttractivenessIndexService,
  49. public attractivenessClustersService: AttractivenessClustersService,
  50. public hsDialogContainerService: HsDialogContainerService,
  51. public hsToastService: HsToastService,
  52. public hsUtilsService: HsUtilsService,
  53. public httpClient: HttpClient
  54. ) {
  55. // First safely set configurable properties
  56. this.allowedClusteringMethods =
  57. attractivenessConfig?.allowedClusteringMethods ?? [];
  58. this.initialWeights = attractivenessConfig?.initialWeights ?? {};
  59. this.serviceBaseUrl = this.adjusterPresetsService.serviceBaseUrl;
  60. this.methods = clusteringMethods.filter((m) =>
  61. this.allowedClusteringMethods.includes(m.codename)
  62. );
  63. this.numberOfClusters = 12;
  64. /* Get the ontology file from the service */
  65. this.loadOntology();
  66. /* Wait for all layers to be ready */
  67. this.adjusterEventService.layerReady.subscribe(({name}) => {
  68. console.log(name + ' ready!');
  69. this.layersReady.add(name);
  70. /* Layers for each method + layer for index are ready */
  71. if (this.layersReady.size == this.methods.length + 1) {
  72. this.adjusterEventService.layerReady.complete();
  73. }
  74. });
  75. /* Ensure that all layers, the loader component and the presets from ontology are ready */
  76. forkJoin({
  77. lyr: this.adjusterEventService.layerReady,
  78. load: this.adjusterEventService.loaderReady,
  79. ont: this.adjusterEventService.ontologyLoads,
  80. }).subscribe(() => {
  81. console.log('Oll layers Korekt! Initializing adjuster...');
  82. //this._loadInProcess = false;
  83. this.init();
  84. });
  85. /* Listen to schema changes so the factors can be re-arranged in the view */
  86. this.adjusterPresetsService.schemaChanges.subscribe((newSchema) => {
  87. const orphanedDatasets = [];
  88. this.factors = newSchema.groups.map((group) => {
  89. const datasets = this.adjusterPresetsService.getGroupDatasets(group.id);
  90. if (datasets.length < 3) {
  91. orphanedDatasets.push(...datasets);
  92. }
  93. return {
  94. id: group.id,
  95. labels: group.labels,
  96. weight: this.resetFactorWeights(group.id),
  97. datasets: this.adjusterPresetsService.getGroupDatasets(group.id),
  98. };
  99. });
  100. this.factors = this.factors.filter(
  101. (factor) => factor.datasets.length >= 3
  102. );
  103. this.factors.push({
  104. id: 'others',
  105. labels: [
  106. {'@value': 'Ostatní', '@language': 'cs'},
  107. {'@value': 'Others', '@language': 'en'},
  108. ],
  109. weight: this.resetFactorWeights('others'),
  110. datasets: orphanedDatasets,
  111. });
  112. this.adjusterLegendService.updateIndexLayerPopUps(this.factors);
  113. });
  114. /* Listen to problem changes so the datasets can be turned on/off */
  115. this.adjusterPresetsService.problemChanges.subscribe((newProblem) => {
  116. if (!newProblem) {
  117. return;
  118. }
  119. for (const factor of this.factors) {
  120. for (const dataset of factor.datasets) {
  121. dataset.included = false;
  122. if (newProblem.requiredDatasets.includes(dataset.id)) {
  123. dataset.included = true;
  124. }
  125. }
  126. }
  127. });
  128. }
  129. /**
  130. * Sends a request to polirural-attractiveness-service
  131. * and applies the returned values
  132. */
  133. apply(): void {
  134. if (this.allowIndex) {
  135. this.calculateIndex();
  136. }
  137. if (this.allowClusters) {
  138. this.calculateClusters();
  139. }
  140. }
  141. async calculateIndex(): Promise<void> {
  142. this._raiInProcess = true;
  143. let attractivenessData: any;
  144. try {
  145. attractivenessData = await this.httpClient
  146. .post(this.serviceBaseUrl + 'eu/scores/', {
  147. factors: this.factors.map((f) => {
  148. return {
  149. factor: f.id,
  150. weight: f.weight,
  151. datasets: f.datasets
  152. .filter((ds) => ds.included)
  153. .map((ds) => ds.id),
  154. };
  155. }),
  156. })
  157. .toPromise();
  158. } catch (error) {
  159. this.hsToastService.createToastPopupMessage(
  160. 'Error loading data',
  161. `Error obtaining data from ${this.serviceBaseUrl}.`
  162. );
  163. console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`);
  164. console.log(error);
  165. this._raiInProcess = false;
  166. this.adjusterEventService.loaded.next({
  167. success: true,
  168. type: 'index',
  169. err: error,
  170. });
  171. return;
  172. }
  173. // Spread the 'aggregate' value between 0 and 1
  174. const min = attractivenessData.reduce((a, b) =>
  175. a.aggregate < b.aggregate ? a : b
  176. ).aggregate;
  177. const max = attractivenessData.reduce((a, b) =>
  178. a.aggregate > b.aggregate ? a : b
  179. ).aggregate;
  180. const coefficient = 1 / (max - min);
  181. const constant = -min * coefficient;
  182. attractivenessData.forEach((a) => {
  183. a.aggregate *= coefficient;
  184. a.aggregate += constant;
  185. });
  186. // Store relation between region and its data in a hash-table-like structure
  187. // more memory consuming, but faster then find()
  188. const codeRecordRelations = {};
  189. attractivenessData.forEach((a) => {
  190. codeRecordRelations[a.code.toUpperCase()] = a;
  191. });
  192. console.time('forEach-Index');
  193. this.attractivenessIndexService.processIndex(
  194. nuts3IndexSource,
  195. codeRecordRelations
  196. );
  197. console.timeEnd('forEach-Index');
  198. this._raiInProcess = false;
  199. this.adjusterEventService.loaded.next({
  200. success: true,
  201. type: 'index',
  202. });
  203. }
  204. async calculateClusters(): Promise<void> {
  205. this._clusteringInProcess = true;
  206. /* Pre-process the API params */
  207. const params = [];
  208. for (const factor of this.factors) {
  209. for (const dataset of factor.datasets) {
  210. if (!dataset.included) {
  211. continue;
  212. }
  213. const flattenedDataset = {
  214. id: dataset.id.split('/').slice(-1).pop(), //We do not need full URIs as the URNs are unique across the ontology
  215. factor: factor.id.split('/').slice(-1).pop(), //We do not need full URIs as the URNs are unique across the ontology
  216. weight: factor.weight,
  217. };
  218. params.push(flattenedDataset);
  219. }
  220. }
  221. let data: any;
  222. try {
  223. data = await this.httpClient
  224. .post(this.serviceBaseUrl + 'eu/clusters/', {
  225. numberOfClusters: this.numberOfClusters,
  226. datasets: params,
  227. })
  228. .toPromise();
  229. } catch (error) {
  230. this.hsToastService.createToastPopupMessage(
  231. 'Error loading data',
  232. `Error obtaining data from ${this.serviceBaseUrl}.`
  233. );
  234. console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`);
  235. console.log(error);
  236. this._clusteringInProcess = false;
  237. this.adjusterEventService.loaded.next({
  238. success: false,
  239. type: 'clusters',
  240. err: error,
  241. });
  242. return;
  243. }
  244. const clusterData = data.response;
  245. // Store relation between region and its data in a hash-table-like structure
  246. // more memory consuming, but much faster then find()
  247. const codeRecordRelations = {};
  248. clusterData.forEach((c) => {
  249. codeRecordRelations[c['nuts_id'].toUpperCase()] = c;
  250. });
  251. console.time('forEach-Cluster');
  252. /*const clusters = [];
  253. for (const region of clusterData) {
  254. if (!clusters.includes(region[this.method])) {
  255. clusters.push(region[this.method]);
  256. }
  257. }*/
  258. for (const method of this.methods) {
  259. this.attractivenessClustersService.processClusters(
  260. method,
  261. codeRecordRelations
  262. );
  263. // Fake the legend
  264. (method.layer.getSource() as any).legend_categories =
  265. this.adjusterLegendService.createClusterLegend(this.numberOfClusters);
  266. }
  267. console.timeEnd('forEach-Cluster');
  268. /*let max = 0;
  269. this.clusters.forEach((a) => {
  270. if (a.aggregate > max) {
  271. max = a.aggregate;
  272. }
  273. });
  274. const normalizer = 1 / max;
  275. this.attractivity.forEach((a) => {
  276. a.aggregate *= normalizer;
  277. });
  278. this.attractivity.forEach((a) => {
  279. this.nutsCodeRecordRelations[a.code] = a;
  280. });*/
  281. // Fake the legend
  282. //nuts.nuts3ClustersSource.legend_categories =
  283. // this.adjusterLegendService.createClusterLegend(this.numberOfClusters);
  284. this._clustersLoaded = true;
  285. this._clusteringInProcess = false;
  286. this.adjusterEventService.loaded.next({
  287. success: true,
  288. type: 'clusters',
  289. });
  290. }
  291. /**
  292. * On init, calculate both index and clusters nevertheless if they are enabled or disabled in the UI
  293. */
  294. async init(): Promise<void> {
  295. this.calculateIndex();
  296. this.calculateClusters();
  297. // In HSL 2.5, setting layer greyscale breaks the print() functionality
  298. //this.hsLayerManagerService.setGreyscale(osmLayer);
  299. }
  300. async loadOntology() {
  301. try {
  302. const onto = await this.httpClient
  303. .get<RDFSubject[]>(this.serviceBaseUrl + 'ontology/')
  304. .toPromise();
  305. this.adjusterEventService.ontologyLoads.next(onto);
  306. this.adjusterEventService.ontologyLoads.complete();
  307. } catch (error) {
  308. this.hsToastService.createToastPopupMessage(
  309. 'Error loading ontology',
  310. `Web service at ${this.serviceBaseUrl} unavailable!`
  311. );
  312. console.warn(`Web service at ${this.serviceBaseUrl} unavailable!`);
  313. console.log(error);
  314. this._loadInProcess = false;
  315. }
  316. this._loadInProcess = false;
  317. }
  318. resetFactorWeights(factorId: string): number {
  319. return this.initialWeights[factorId] ?? 0.5;
  320. }
  321. clustersLoaded(): boolean {
  322. return this._clustersLoaded;
  323. }
  324. /**
  325. * @returns {boolean} true if clustering or index processing is in process or loading data, false otherwise
  326. */
  327. isInProcess(): boolean {
  328. return (
  329. this._loadInProcess || this._clusteringInProcess || this._raiInProcess
  330. );
  331. }
  332. isLoading(): boolean {
  333. return this._loadInProcess;
  334. }
  335. isClustering(): boolean {
  336. return this._clusteringInProcess;
  337. }
  338. isCalculatingRAI(): boolean {
  339. return this._raiInProcess;
  340. }
  341. }