adjuster.service.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  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. 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((factor) => factor.datasets.length >= 3);
  101. this.factors.push({
  102. id: 'others',
  103. labels: [{'@value': 'Ostatní', '@language': 'cs'}, {'@value': 'Others', '@language': 'en'}],
  104. weight: this.resetFactorWeights('others'),
  105. datasets: orphanedDatasets,
  106. })
  107. this.adjusterLegendService.updateIndexLayerPopUps(this.factors);
  108. });
  109. /* Listen to problem changes so the datasets can be turned on/off */
  110. this.adjusterPresetsService.problemChanges.subscribe((newProblem) => {
  111. if (!newProblem) {
  112. return;
  113. }
  114. for (const factor of this.factors) {
  115. for (const dataset of factor.datasets) {
  116. dataset.included = false;
  117. if (newProblem.requiredDatasets.includes(dataset.id)) {
  118. dataset.included = true;
  119. }
  120. }
  121. }
  122. });
  123. }
  124. /**
  125. * Sends a request to polirural-attractiveness-service
  126. * and applies the returned values
  127. */
  128. apply(): void {
  129. if (this.allowIndex) {
  130. this.calculateIndex();
  131. }
  132. if (this.allowClusters) {
  133. this.calculateClusters();
  134. }
  135. }
  136. calculateIndex(): void {
  137. this._raiInProcess = true;
  138. this.$http
  139. .post(this.serviceBaseUrl + 'cz/scores/', {
  140. factors: this.factors.map((f) => {
  141. return {
  142. factor: f.id,
  143. weight: f.weight,
  144. datasets: f.datasets
  145. .filter((ds) => ds.included)
  146. .map((ds) => ds.id),
  147. };
  148. }),
  149. })
  150. .toPromise()
  151. .then((attractivenessData: any[]) => {
  152. console.log("attractivenessData", attractivenessData)
  153. /* Spread the 'aggregate' value between 0 and 1 */
  154. const min = attractivenessData.reduce((a, b) =>
  155. a.aggregate < b.aggregate ? a : b
  156. ).aggregate;
  157. const max = attractivenessData.reduce((a, b) =>
  158. a.aggregate > b.aggregate ? a : b
  159. ).aggregate;
  160. const coefficient = 1 / (max - min);
  161. const constant = -min * coefficient;
  162. attractivenessData.forEach((a) => {
  163. a.aggregate *= coefficient;
  164. a.aggregate += constant;
  165. });
  166. /* Store relation between region and its data in a hash-table-like structure
  167. * More memory consuming, but faster then find() */
  168. const codeRecordRelations = {};
  169. attractivenessData.forEach((a) => {
  170. codeRecordRelations[a.code] = a;
  171. });
  172. console.time('forEach-Index');
  173. this.processIndex(codeRecordRelations);
  174. console.timeEnd('forEach-Index');
  175. this._raiInProcess = false;
  176. this.adjusterEventService.loaded.next({
  177. success: true,
  178. type: 'index',
  179. });
  180. })
  181. .catch((error) => {
  182. this.hsToastService.createToastPopupMessage('Error loading data', `Error obtaining data from ${this.serviceBaseUrl}.`);
  183. console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`);
  184. console.log(error);
  185. this._raiInProcess = false;
  186. this.adjusterEventService.loaded.next({
  187. success: true,
  188. type: 'index',
  189. err: error,
  190. });
  191. });
  192. }
  193. calculateClusters(): void {
  194. this._clusteringInProcess = true;
  195. /* Pre-process the API params */
  196. const params = [];
  197. for (const factor of this.factors) {
  198. for (const dataset of factor.datasets) {
  199. if (!dataset.included) {
  200. continue;
  201. }
  202. const flattenedDataset = {
  203. id: dataset.id.split('/').slice(-1).pop(), //We do not need full URIs as the URNs are unique across the ontology
  204. factor: factor.id.split('/').slice(-1).pop(), //We do not need full URIs as the URNs are unique across the ontology
  205. weight: factor.weight
  206. };
  207. params.push(flattenedDataset)
  208. }
  209. }
  210. this.$http
  211. .post(this.serviceBaseUrl + 'cz/clusters/', {
  212. numberOfClusters: this.numberOfClusters,
  213. datasets: params
  214. })
  215. .toPromise()
  216. .then((data: any) => {
  217. const clusterData = data.response;
  218. /* Store relation between region and its data in a hash-table-like structure
  219. * more memory consuming, but much faster then find() */
  220. const codeRecordRelations = {};
  221. clusterData.forEach((c) => {
  222. codeRecordRelations[c.lau2] = c;
  223. });
  224. /*let sublayers = [];
  225. const oldSublayers = obceLayer.get('Layer');
  226. if (oldSublayers !== undefined && Array.isArray(oldSublayers)) {
  227. for (const sublyr of oldSublayers) {
  228. sublyr.getSource().clear();
  229. }
  230. if (oldSublayers.length > this.numberOfClusters) {
  231. sublayers = oldSublayers.slice(0, this.numberOfClusters);
  232. }
  233. }
  234. let i = sublayers.length;
  235. while (sublayers.length < this.numberOfClusters) {
  236. const sublyr = new VectorLayer({
  237. title: `Cluster ${i + 1}`,
  238. name: i + 1 + '',
  239. source: new VectorSource({}),
  240. });
  241. sublayers.push(sublyr);
  242. sublyr.Title = `Cluster ${i + 1}`;
  243. sublyr.Name = `c${i + 1}`;
  244. i++;
  245. }*/
  246. console.time('forEach-Cluster');
  247. for (const method of this.methods) {
  248. this.processClusters(method, codeRecordRelations);
  249. method.layer.getSource().legend_categories = this.adjusterLegendService.createClusterLegend(
  250. this.numberOfClusters
  251. );
  252. }
  253. // Another implementation of the loop above, yet too slow
  254. //const clusters = [];
  255. //const obceFeatures: Array<any> = obce.getFeatures();
  256. // Array of arrays
  257. /*const methodsFeatures = this.methods.map((m) =>
  258. m.layer.getSource().getFeatures()
  259. );
  260. for (const region of clusterData) {
  261. //if (!clusters.includes(region[this.method])) {
  262. // clusters.push(region[this.method]);
  263. //}
  264. for (const features of methodsFeatures) {
  265. const feature = features.find(
  266. // NOTE: Do NOT add triple equal sign!
  267. (f) => f.get('nationalCode') == region['lau2']
  268. );
  269. if (!feature) {
  270. if (errs < 20) {
  271. errs++;
  272. console.warn(`No feature matches region ${region['lau2']}`);
  273. console.log(region);
  274. }
  275. continue;
  276. }
  277. Object.keys(region).forEach(function (key, index) {
  278. if (key !== 'lau2') {
  279. feature.set(key, region[key], true);
  280. }
  281. });
  282. }
  283. logs++;
  284. if (logs % 100 == 0) {
  285. console.log(`processed ${logs} items`);
  286. }
  287. //sublayers[region[this.method] - 1].getSource().addFeature(feature);
  288. }*/
  289. //obceLayer.set('Layer', sublayers);
  290. //this.hsLayerMetadataService.fillMetadata(obceLayer);
  291. //console.log(sublayers[0].getSource().getFeatures());
  292. console.timeEnd('forEach-Cluster');
  293. //this.clusters = clusters;
  294. this._clustersLoaded = true;
  295. this._clusteringInProcess = false;
  296. this.adjusterEventService.loaded.next({
  297. success: true,
  298. type: 'clusters',
  299. });
  300. })
  301. .catch((error) => {
  302. this.hsToastService.createToastPopupMessage('Error loading data', `Error obtaining data from ${this.serviceBaseUrl}.`);
  303. console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`);
  304. console.log(error);
  305. this._clusteringInProcess = false;
  306. this.adjusterEventService.loaded.next({
  307. success: false,
  308. type: 'clusters',
  309. err: error,
  310. });
  311. });
  312. }
  313. init(): void {
  314. this._loadInProcess = true;
  315. this.$http
  316. .get(this.serviceBaseUrl + 'cz/datasets/')
  317. .toPromise()
  318. .then((data: any) => {
  319. //console.log(data);
  320. /*this.factors = data.map((dataset) => {
  321. return {
  322. name: dataset.Factor,
  323. weight: this.initialWeights[dataset.Factor] ?? 1,
  324. datasets: [],
  325. };
  326. });
  327. this.factors = this.hsUtilsService.removeDuplicates(
  328. this.factors,
  329. 'name'
  330. );
  331. this.factors.forEach((factor) => {
  332. factor.datasets = data
  333. .filter((ds) => ds.Factor === factor.name)
  334. .map((ds) => {
  335. return {
  336. name: ds.Name,
  337. desc: ds.Description,
  338. included: true,
  339. };
  340. });
  341. });*/
  342. this._loadInProcess = false;
  343. this.apply();
  344. // In HSL 2.5, setting layer greyscale breaks the print() functionality
  345. //this.hsLayerManagerService.setGreyscale(osmLayer);
  346. })
  347. .catch((error) => {
  348. this.hsToastService.createToastPopupMessage('Error loading data', `Web service at ${this.serviceBaseUrl} unavailable!`);
  349. console.warn(`Web service at ${this.serviceBaseUrl} unavailable!`);
  350. console.log(error);
  351. this._loadInProcess = false;
  352. });
  353. }
  354. async loadOntology() {
  355. try {
  356. const onto = await this.$http.get<RDFSubject[]>(this.serviceBaseUrl + 'ontology/').toPromise();
  357. this.adjusterEventService.ontologyLoads.next(onto);
  358. this.adjusterEventService.ontologyLoads.complete();
  359. } catch (error) {
  360. this.hsToastService.createToastPopupMessage('Error loading ontology', `Web service at ${this.serviceBaseUrl} unavailable!`);
  361. console.warn(`Web service at ${this.serviceBaseUrl} unavailable!`);
  362. console.log(error);
  363. this._loadInProcess = false;
  364. }
  365. }
  366. processIndex(codeRecordRelations: Record<string, unknown>): void {
  367. /*if (obce.getFeatures()?.length < 1) {
  368. obce.once('changefeature', () => this.processIndex(codeRecordRelations));
  369. return;
  370. }*/
  371. let errs = 0;
  372. //let logs = 0;
  373. obce.forEachFeature((feature) => {
  374. // Pair each feature with its attractivity data
  375. const featureData = codeRecordRelations[feature.get('nationalCode')];
  376. if (!featureData) {
  377. if (errs < 20) {
  378. errs++;
  379. console.warn(`No data for feature ${feature.get('nationalCode')}`);
  380. console.log(feature);
  381. }
  382. return;
  383. }
  384. /*logs++;
  385. if (logs % 100 == 0) {
  386. console.log(`processed ${logs} items`);
  387. }*/
  388. Object.keys(featureData).forEach((key, index) => {
  389. if (key !== 'lau2') {
  390. feature.set(key, featureData[key], true); //true stands for "silent" - important for performance!
  391. }
  392. });
  393. });
  394. // Since we are updating the features silently, we now have to refresh manually
  395. obce.getFeatures()[0].dispatchEvent('change');
  396. }
  397. processClusters(
  398. method: MethodDescription,
  399. codeRecordRelations: Record<string, unknown>
  400. ): void {
  401. /*if (method.layer?.getSource().getFeatures()?.length < 1) {
  402. method.layer
  403. .getSource()
  404. .once('changefeature', () =>
  405. this.processClusters(method, codeRecordRelations)
  406. );
  407. return;
  408. }*/
  409. let errs = 0;
  410. //let logs = 0;
  411. method.layer.getSource().forEachFeature((feature) => {
  412. // Pair each feature with its clustering data
  413. const featureData = codeRecordRelations[feature.get('nationalCode')];
  414. /*const featureData = clusterData.find(
  415. // NOTE: Do NOT add triple equal sign!
  416. (item) => item['lau2'] == feature.get('nationalCode')
  417. );*/
  418. if (!featureData) {
  419. if (errs < 20) {
  420. errs++;
  421. console.warn(`No data for feature ${feature.get('nationalCode')}`);
  422. console.log(feature);
  423. }
  424. return;
  425. }
  426. /*logs++;
  427. if (logs % 100 == 0) {
  428. console.log(`processed ${logs} items`);
  429. }*/
  430. feature.set(method.codename, featureData[method.codename], true);
  431. /*Object.keys(featureData).forEach(function (key, index) {
  432. if (key !== 'lau2') {
  433. feature.set(key, featureData[key], true);
  434. }
  435. });*/
  436. });
  437. // Since we are updating the features silently, we now have to refresh manually
  438. method.layer.getSource().getFeatures()[0].dispatchEvent('change');
  439. }
  440. resetFactorWeights(factorId: string): number {
  441. return this.initialWeights[factorId] ?? 0.5;
  442. }
  443. clustersLoaded(): boolean {
  444. return this._clustersLoaded;
  445. }
  446. /**
  447. * @returns {boolean} true if clustering or index processing is in process or loading data, false otherwise
  448. */
  449. isInProcess(): boolean {
  450. return (
  451. this._loadInProcess || this._clusteringInProcess || this._raiInProcess
  452. );
  453. }
  454. isLoading(): boolean {
  455. return this._loadInProcess;
  456. }
  457. isClustering(): boolean {
  458. return this._clusteringInProcess;
  459. }
  460. isCalculatingRAI(): boolean {
  461. return this._raiInProcess;
  462. }
  463. }
  464. type MethodDescription = {
  465. codename: string;
  466. layer?: VectorLayer<any>;
  467. name: string;
  468. type: string;
  469. };