adjuster.service.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. import {HttpClient} from '@angular/common/http';
  2. import {Injectable} from '@angular/core';
  3. import {Vector as VectorLayer} from 'ol/layer';
  4. import {forkJoin} from 'rxjs';
  5. import {HsConfig} from 'hslayers-ng';
  6. import {HsEventBusService} from 'hslayers-ng';
  7. //import {HsLayerManagerMetadataService} from 'hslayers-ng/components/layermanager/layermanager-metadata.service';
  8. import {HsLayerManagerService} from 'hslayers-ng';
  9. import {HsToastService} from 'hslayers-ng';
  10. import {HsUtilsService} from 'hslayers-ng';
  11. import attractivenessConfig from '../attractiveness.config.json';
  12. import clusteringMethods from '../data/clustering_methods.json';
  13. import {AdjusterEventService} from './adjuster-event.service';
  14. import {AdjusterLegendService} from './adjuster-legend.service';
  15. import {AdjusterPresetsService, Factor} from './adjuster-presets.service';
  16. import {obce, obceIndexLayer, osmLayer} from '../app.config';
  17. import {RDFSubject} from './ontology.model';
  18. @Injectable({providedIn: 'root'})
  19. export class AdjusterService {
  20. /** To be read from a config file */
  21. allowedClusteringMethods: Array<string>;
  22. /** To be read from a config file */
  23. initialWeights: {[factorId: string]: number};
  24. /** To be read from a config file */
  25. serviceBaseUrl: string;
  26. /** Used in the UI as a selector */
  27. allowClusters = true;
  28. /** Used in the UI as a selector */
  29. allowIndex = true;
  30. factors: Array<Factor> = [];
  31. layersReady = new Set();
  32. //method: string;
  33. methods: Array<MethodDescription>;
  34. numberOfClusters: number;
  35. private _clusteringInProcess: boolean;
  36. private _clustersLoaded: boolean;
  37. /** Once instantiated, the load is definitely in process */
  38. private _loadInProcess = true;
  39. private _raiInProcess: boolean;
  40. constructor(
  41. public adjusterEventService: AdjusterEventService,
  42. public adjusterLegendService: AdjusterLegendService,
  43. public adjusterPresetsService: AdjusterPresetsService,
  44. public hsConfig: HsConfig,
  45. public hsEventBus: HsEventBusService,
  46. //public hsLayerMetadataService: HsLayerManagerMetadataService,
  47. public hsLayerManagerService: HsLayerManagerService,
  48. public hsToastService: HsToastService,
  49. public hsUtilsService: HsUtilsService,
  50. public $http: HttpClient
  51. ) {
  52. /* First safely set configurable properties */
  53. this.allowedClusteringMethods =
  54. attractivenessConfig?.allowedClusteringMethods ?? [];
  55. this.initialWeights = attractivenessConfig?.initialWeights ?? {};
  56. this.serviceBaseUrl =
  57. attractivenessConfig?.serviceBaseUrl ??
  58. 'https://publish.lesprojekt.cz/nodejs/';
  59. // 'https://jmacura.ml/ws/' // 'http://localhost:3000/'
  60. this.methods = clusteringMethods.filter((m) =>
  61. this.allowedClusteringMethods.includes(m.codename)
  62. );
  63. this.numberOfClusters = 9;
  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. //TODO: i18n this!
  88. this.factors = newSchema.groups.map((group) => {
  89. return {
  90. id: group.id,
  91. labels: group.labels,
  92. weight: this.resetFactorWeights(group.id),
  93. datasets: this.adjusterPresetsService.getGroupDatasets(group.id)
  94. }
  95. });
  96. });
  97. /* Listen to problem changes so the datasets can be turned on/off */
  98. this.adjusterPresetsService.problemChanges.subscribe((newProblem) => {
  99. if (!newProblem) {
  100. return;
  101. }
  102. for (const factor of this.factors) {
  103. for (const dataset of factor.datasets) {
  104. dataset.included = false;
  105. if (newProblem.requiredDatasets.includes(dataset.id)) {
  106. dataset.included = true;
  107. }
  108. }
  109. }
  110. });
  111. }
  112. /**
  113. * Sends a request to polirural-attractiveness-service
  114. * and applies the returned values
  115. */
  116. apply(): void {
  117. if (this.allowIndex) {
  118. this.calculateIndex();
  119. }
  120. if (this.allowClusters) {
  121. this.calculateClusters();
  122. }
  123. }
  124. calculateIndex(): void {
  125. this._raiInProcess = true;
  126. this.$http
  127. .post(this.serviceBaseUrl + 'cz/scores/', {
  128. factors: this.factors.map((f) => {
  129. return {
  130. factor: f.id,
  131. weight: f.weight,
  132. datasets: f.datasets
  133. .filter((ds) => ds.included)
  134. .map((ds) => ds.id),
  135. };
  136. }),
  137. })
  138. .toPromise()
  139. .then((attractivenessData: any[]) => {
  140. /* Spread the 'aggregate' value between 0 and 1 */
  141. const min = attractivenessData.reduce((a, b) =>
  142. a.aggregate < b.aggregate ? a : b
  143. ).aggregate;
  144. const max = attractivenessData.reduce((a, b) =>
  145. a.aggregate > b.aggregate ? a : b
  146. ).aggregate;
  147. const coefficient = 1 / (max - min);
  148. const constant = -min * coefficient;
  149. attractivenessData.forEach((a) => {
  150. a.aggregate *= coefficient;
  151. a.aggregate += constant;
  152. });
  153. /* Store relation between region and its data in a hash-table-like structure
  154. * More memory consuming, but faster then find() */
  155. const codeRecordRelations = {};
  156. attractivenessData.forEach((a) => {
  157. codeRecordRelations[a.code] = a;
  158. });
  159. console.time('forEach-Index');
  160. this.processIndex(codeRecordRelations);
  161. console.timeEnd('forEach-Index');
  162. this._raiInProcess = false;
  163. this.adjusterEventService.loaded.next({
  164. success: true,
  165. type: 'index',
  166. });
  167. })
  168. .catch((error) => {
  169. this.hsToastService.createToastPopupMessage('Error loading data', `Error obtaining data from ${this.serviceBaseUrl}.`);
  170. console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`);
  171. console.log(error);
  172. this._raiInProcess = false;
  173. this.adjusterEventService.loaded.next({
  174. success: true,
  175. type: 'index',
  176. err: error,
  177. });
  178. });
  179. }
  180. calculateClusters(): void {
  181. this._clusteringInProcess = true;
  182. /* Pre-process the API params */
  183. const params = [];
  184. for (const factor of this.factors) {
  185. for (const dataset of factor.datasets) {
  186. if (!dataset.included) {
  187. continue;
  188. }
  189. const flattenedDataset = {
  190. id: dataset.id.split('/').slice(-1).pop(), //We do not need full URIs as the URNs are unique across the ontology
  191. factor: factor.id.split('/').slice(-1).pop(), //We do not need full URIs as the URNs are unique across the ontology
  192. weight: factor.weight
  193. };
  194. params.push(flattenedDataset)
  195. }
  196. }
  197. this.$http
  198. .post(this.serviceBaseUrl + 'cz/clusters/', {
  199. numberOfClusters: this.numberOfClusters,
  200. datasets: params
  201. })
  202. .toPromise()
  203. .then((data: any) => {
  204. const clusterData = data.response;
  205. /* Store relation between region and its data in a hash-table-like structure
  206. * more memory consuming, but much faster then find() */
  207. const codeRecordRelations = {};
  208. clusterData.forEach((c) => {
  209. codeRecordRelations[c.lau2] = c;
  210. });
  211. /*let sublayers = [];
  212. const oldSublayers = obceLayer.get('Layer');
  213. if (oldSublayers !== undefined && Array.isArray(oldSublayers)) {
  214. for (const sublyr of oldSublayers) {
  215. sublyr.getSource().clear();
  216. }
  217. if (oldSublayers.length > this.numberOfClusters) {
  218. sublayers = oldSublayers.slice(0, this.numberOfClusters);
  219. }
  220. }
  221. let i = sublayers.length;
  222. while (sublayers.length < this.numberOfClusters) {
  223. const sublyr = new VectorLayer({
  224. title: `Cluster ${i + 1}`,
  225. name: i + 1 + '',
  226. source: new VectorSource({}),
  227. });
  228. sublayers.push(sublyr);
  229. sublyr.Title = `Cluster ${i + 1}`;
  230. sublyr.Name = `c${i + 1}`;
  231. i++;
  232. }*/
  233. console.time('forEach-Cluster');
  234. for (const method of this.methods) {
  235. this.processClusters(method, codeRecordRelations);
  236. method.layer.getSource().legend_categories = this.adjusterLegendService.createClusterLegend(
  237. this.numberOfClusters
  238. );
  239. }
  240. // Another implementation of the loop above, yet too slow
  241. //const clusters = [];
  242. //const obceFeatures: Array<any> = obce.getFeatures();
  243. // Array of arrays
  244. /*const methodsFeatures = this.methods.map((m) =>
  245. m.layer.getSource().getFeatures()
  246. );
  247. for (const region of clusterData) {
  248. //if (!clusters.includes(region[this.method])) {
  249. // clusters.push(region[this.method]);
  250. //}
  251. for (const features of methodsFeatures) {
  252. const feature = features.find(
  253. // NOTE: Do NOT add triple equal sign!
  254. (f) => f.get('nationalCode') == region['lau2']
  255. );
  256. if (!feature) {
  257. if (errs < 20) {
  258. errs++;
  259. console.warn(`No feature matches region ${region['lau2']}`);
  260. console.log(region);
  261. }
  262. continue;
  263. }
  264. Object.keys(region).forEach(function (key, index) {
  265. if (key !== 'lau2') {
  266. feature.set(key, region[key], true);
  267. }
  268. });
  269. }
  270. logs++;
  271. if (logs % 100 == 0) {
  272. console.log(`processed ${logs} items`);
  273. }
  274. //sublayers[region[this.method] - 1].getSource().addFeature(feature);
  275. }*/
  276. //obceLayer.set('Layer', sublayers);
  277. //this.hsLayerMetadataService.fillMetadata(obceLayer);
  278. //console.log(sublayers[0].getSource().getFeatures());
  279. console.timeEnd('forEach-Cluster');
  280. //this.clusters = clusters;
  281. this._clustersLoaded = true;
  282. this._clusteringInProcess = false;
  283. this.adjusterEventService.loaded.next({
  284. success: true,
  285. type: 'clusters',
  286. });
  287. })
  288. .catch((error) => {
  289. this.hsToastService.createToastPopupMessage('Error loading data', `Error obtaining data from ${this.serviceBaseUrl}.`);
  290. console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`);
  291. console.log(error);
  292. this._clusteringInProcess = false;
  293. this.adjusterEventService.loaded.next({
  294. success: false,
  295. type: 'clusters',
  296. err: error,
  297. });
  298. });
  299. }
  300. init(): void {
  301. this._loadInProcess = true;
  302. this.$http
  303. .get(this.serviceBaseUrl + 'cz/datasets/')
  304. .toPromise()
  305. .then((data: any) => {
  306. //console.log(data);
  307. /*this.factors = data.map((dataset) => {
  308. return {
  309. name: dataset.Factor,
  310. weight: this.initialWeights[dataset.Factor] ?? 1,
  311. datasets: [],
  312. };
  313. });
  314. this.factors = this.hsUtilsService.removeDuplicates(
  315. this.factors,
  316. 'name'
  317. );
  318. this.factors.forEach((factor) => {
  319. factor.datasets = data
  320. .filter((ds) => ds.Factor === factor.name)
  321. .map((ds) => {
  322. return {
  323. name: ds.Name,
  324. desc: ds.Description,
  325. included: true,
  326. };
  327. });
  328. });*/
  329. this._loadInProcess = false;
  330. this.apply();
  331. // In HSL 2.5, setting layer greyscale breaks the print() functionality
  332. //this.hsLayerManagerService.setGreyscale(osmLayer);
  333. })
  334. .catch((error) => {
  335. this.hsToastService.createToastPopupMessage('Error loading data', `Web service at ${this.serviceBaseUrl} unavailable!`);
  336. console.warn(`Web service at ${this.serviceBaseUrl} unavailable!`);
  337. console.log(error);
  338. this._loadInProcess = false;
  339. });
  340. }
  341. async loadOntology() {
  342. try {
  343. const onto = await this.$http.get<RDFSubject[]>(this.serviceBaseUrl + 'ontology/').toPromise();
  344. this.adjusterEventService.ontologyLoads.next(onto);
  345. this.adjusterEventService.ontologyLoads.complete();
  346. } catch (error) {
  347. this.hsToastService.createToastPopupMessage('Error loading ontology', `Web service at ${this.serviceBaseUrl} unavailable!`);
  348. console.warn(`Web service at ${this.serviceBaseUrl} unavailable!`);
  349. console.log(error);
  350. this._loadInProcess = false;
  351. }
  352. }
  353. processIndex(codeRecordRelations: Record<string, unknown>): void {
  354. /*if (obce.getFeatures()?.length < 1) {
  355. obce.once('changefeature', () => this.processIndex(codeRecordRelations));
  356. return;
  357. }*/
  358. let errs = 0;
  359. //let logs = 0;
  360. obce.forEachFeature((feature) => {
  361. // Pair each feature with its attractivity data
  362. const featureData = codeRecordRelations[feature.get('nationalCode')];
  363. if (!featureData) {
  364. if (errs < 20) {
  365. errs++;
  366. console.warn(`No data for feature ${feature.get('nationalCode')}`);
  367. console.log(feature);
  368. }
  369. return;
  370. }
  371. /*logs++;
  372. if (logs % 100 == 0) {
  373. console.log(`processed ${logs} items`);
  374. }*/
  375. Object.keys(featureData).forEach((key, index) => {
  376. if (key !== 'lau2') {
  377. feature.set(key, featureData[key], true); //true stands for "silent" - important for performance!
  378. }
  379. });
  380. });
  381. // Since we are updating the features silently, we now have to refresh manually
  382. obce.getFeatures()[0].dispatchEvent('change');
  383. }
  384. processClusters(
  385. method: MethodDescription,
  386. codeRecordRelations: Record<string, unknown>
  387. ): void {
  388. /*if (method.layer?.getSource().getFeatures()?.length < 1) {
  389. method.layer
  390. .getSource()
  391. .once('changefeature', () =>
  392. this.processClusters(method, codeRecordRelations)
  393. );
  394. return;
  395. }*/
  396. let errs = 0;
  397. //let logs = 0;
  398. method.layer.getSource().forEachFeature((feature) => {
  399. // Pair each feature with its clustering data
  400. const featureData = codeRecordRelations[feature.get('nationalCode')];
  401. /*const featureData = clusterData.find(
  402. // NOTE: Do NOT add triple equal sign!
  403. (item) => item['lau2'] == feature.get('nationalCode')
  404. );*/
  405. if (!featureData) {
  406. if (errs < 20) {
  407. errs++;
  408. console.warn(`No data for feature ${feature.get('nationalCode')}`);
  409. console.log(feature);
  410. }
  411. return;
  412. }
  413. /*logs++;
  414. if (logs % 100 == 0) {
  415. console.log(`processed ${logs} items`);
  416. }*/
  417. feature.set(method.codename, featureData[method.codename], true);
  418. /*Object.keys(featureData).forEach(function (key, index) {
  419. if (key !== 'lau2') {
  420. feature.set(key, featureData[key], true);
  421. }
  422. });*/
  423. });
  424. // Since we are updating the features silently, we now have to refresh manually
  425. method.layer.getSource().getFeatures()[0].dispatchEvent('change');
  426. }
  427. resetFactorWeights(factorId: string): number {
  428. return this.initialWeights[factorId] ?? 0.5;
  429. }
  430. clustersLoaded(): boolean {
  431. return this._clustersLoaded;
  432. }
  433. /**
  434. * @returns {boolean} true if clustering or index processing is in process or loading data, false otherwise
  435. */
  436. isInProcess(): boolean {
  437. return (
  438. this._loadInProcess || this._clusteringInProcess || this._raiInProcess
  439. );
  440. }
  441. isLoading(): boolean {
  442. return this._loadInProcess;
  443. }
  444. isClustering(): boolean {
  445. return this._clusteringInProcess;
  446. }
  447. isCalculatingRAI(): boolean {
  448. return this._raiInProcess;
  449. }
  450. }
  451. type MethodDescription = {
  452. codename: string;
  453. layer?: VectorLayer<any>;
  454. name: string;
  455. type: string;
  456. };