adjuster.service.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import {HttpClient} from '@angular/common/http';
  2. import {Injectable} from '@angular/core';
  3. import {Vector as VectorLayer} from 'ol/layer';
  4. import {HsDialogContainerService} from 'hslayers-ng/components/layout/dialogs/dialog-container.service';
  5. import {HsUtilsService} from 'hslayers-ng/components/utils/utils.service';
  6. import attractivenessConfig from '../attractiveness.config.json';
  7. import clusteringMethods from '../data/clustering_methods.json';
  8. import {AdjusterEventService} from './adjuster-event.service';
  9. import {AdjusterLegendService} from './adjuster-legend.service';
  10. import {AdjusterLoaderComponent} from './adjuster-loader.component';
  11. import {AttractivenessClustersService} from './attractiveness-clusters.service';
  12. import {AttractivenessIndexService} from './attractiveness-index.service';
  13. import {nuts} from '../nuts';
  14. @Injectable({providedIn: 'root'})
  15. export class AdjusterService {
  16. /** To be read from a config file */
  17. allowedClusteringMethods: Array<string>;
  18. /** To be read from a config file */
  19. initialWeights: Record<string, number>;
  20. /** To be read from a config file */
  21. serviceBaseUrl: string;
  22. /** Used in the UI as a selector */
  23. allowClusters = true;
  24. /** Used in the UI as a selector */
  25. allowIndex = true;
  26. factors = [];
  27. clusters = [];
  28. numberOfClusters: number;
  29. method: string;
  30. methods: Array<MethodDescription>;
  31. private _clusteringInProcess: boolean;
  32. private _clustersLoaded: boolean;
  33. private _loadInProcess: boolean;
  34. private _raiInProcess: boolean;
  35. constructor(
  36. public adjusterEventService: AdjusterEventService,
  37. public adjusterLegendService: AdjusterLegendService,
  38. public attractivenessIndexService: AttractivenessIndexService,
  39. public attractivenessClustersService: AttractivenessClustersService,
  40. public hsDialogContainerService: HsDialogContainerService,
  41. public hsUtilsService: HsUtilsService,
  42. public httpClient: HttpClient
  43. ) {
  44. // First safely set configurable properties
  45. this.allowedClusteringMethods =
  46. attractivenessConfig?.allowedClusteringMethods ?? [];
  47. this.initialWeights = attractivenessConfig?.initialWeights ?? {};
  48. this.serviceBaseUrl =
  49. attractivenessConfig?.serviceBaseUrl ??
  50. 'https://publish.lesprojekt.cz/nodejs/';
  51. // 'https://jmacura.ml/ws/' // 'http://localhost:3000/'
  52. this.methods = clusteringMethods.filter((m) =>
  53. this.allowedClusteringMethods.includes(m.codename)
  54. );
  55. this.method = 'haclustwd2'; //TODO: set in config/or not use at all?
  56. this.numberOfClusters = 12;
  57. }
  58. /**
  59. * Sends a request to polirural-attractiveness-service
  60. * and applies the returned values
  61. */
  62. apply(): void {
  63. if (this.allowIndex) {
  64. this.calculateIndex();
  65. }
  66. if (this.allowClusters) {
  67. this.calculateClusters();
  68. }
  69. }
  70. async calculateIndex(): Promise<void> {
  71. this._raiInProcess = true;
  72. let attractivenessData: any;
  73. try {
  74. attractivenessData = await this.httpClient
  75. .post(this.serviceBaseUrl + 'eu/scores/', {
  76. factors: this.factors.map((f) => {
  77. return {
  78. factor: f.name,
  79. weight: f.weight,
  80. datasets: f.datasets
  81. .filter((ds) => ds.included)
  82. .map((ds) => ds.name),
  83. };
  84. }),
  85. })
  86. .toPromise();
  87. } catch (error) {
  88. console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`);
  89. console.log(error);
  90. this._raiInProcess = false;
  91. this.adjusterEventService.loaded.next({
  92. success: true,
  93. type: 'index',
  94. err: error,
  95. });
  96. }
  97. // Spread the 'aggregate' value between 0 and 1
  98. const min = attractivenessData.reduce((a, b) =>
  99. a.aggregate < b.aggregate ? a : b
  100. ).aggregate;
  101. const max = attractivenessData.reduce((a, b) =>
  102. a.aggregate > b.aggregate ? a : b
  103. ).aggregate;
  104. const coefficient = 1 / (max - min);
  105. const constant = -min * coefficient;
  106. attractivenessData.forEach((a) => {
  107. a.aggregate *= coefficient;
  108. a.aggregate += constant;
  109. });
  110. // Store relation between region and its data in a hash-table-like structure
  111. // more memory consuming, but faster then find()
  112. const codeRecordRelations = {};
  113. attractivenessData.forEach((a) => {
  114. codeRecordRelations[a.code.toUpperCase()] = a;
  115. });
  116. console.time('forEach-Index');
  117. this.attractivenessIndexService.processIndex(codeRecordRelations);
  118. console.timeEnd('forEach-Index');
  119. this._raiInProcess = false;
  120. this.adjusterEventService.loaded.next({
  121. success: true,
  122. type: 'index',
  123. });
  124. }
  125. calculateClusters(): void {
  126. this._clusteringInProcess = true;
  127. this.httpClient
  128. .post(this.serviceBaseUrl + 'eu/clusters/', {
  129. numberOfClusters: this.numberOfClusters,
  130. factors: this.factors.map((f) => {
  131. return {
  132. factor: f.name,
  133. weight: f.weight,
  134. datasets: f.datasets
  135. .filter((ds) => ds.included)
  136. .map((ds) => ds.name),
  137. };
  138. }),
  139. })
  140. .toPromise()
  141. .then((data: any) => {
  142. const clusterData = data.response;
  143. // Store relation between region and its data in a hash-table-like structure
  144. // more memory consuming, but much faster then find()
  145. const codeRecordRelations = {};
  146. clusterData.forEach((c) => {
  147. codeRecordRelations[c['nuts_id'].toUpperCase()] = c;
  148. });
  149. console.time('forEach-Cluster');
  150. /*const clusters = [];
  151. for (const region of clusterData) {
  152. if (!clusters.includes(region[this.method])) {
  153. clusters.push(region[this.method]);
  154. }
  155. }*/
  156. //for (const method of this.methods) {
  157. this.attractivenessClustersService.processClusters(codeRecordRelations);
  158. //}
  159. console.timeEnd('forEach-Cluster');
  160. /*let max = 0;
  161. this.clusters.forEach((a) => {
  162. if (a.aggregate > max) {
  163. max = a.aggregate;
  164. }
  165. });
  166. const normalizer = 1 / max;
  167. this.attractivity.forEach((a) => {
  168. a.aggregate *= normalizer;
  169. });
  170. this.attractivity.forEach((a) => {
  171. this.nutsCodeRecordRelations[a.code] = a;
  172. });*/
  173. // Fake the legend
  174. nuts.nuts3ClustersSource.legend_categories = this.adjusterLegendService.createClusterLegend(
  175. this.numberOfClusters
  176. );
  177. this._clustersLoaded = true;
  178. this._clusteringInProcess = false;
  179. this.adjusterEventService.loaded.next({
  180. success: true,
  181. type: 'clusters',
  182. });
  183. })
  184. .catch((error) => {
  185. console.warn(`Error obtaining data from ${this.serviceBaseUrl}.`);
  186. console.log(error);
  187. this._clusteringInProcess = false;
  188. this.adjusterEventService.loaded.next({
  189. success: false,
  190. type: 'clusters',
  191. err: error,
  192. });
  193. });
  194. }
  195. init(): void {
  196. this._loadInProcess = true;
  197. this.httpClient
  198. .get(this.serviceBaseUrl + 'eu/datasets/')
  199. .toPromise()
  200. .then((data: any) => {
  201. this.factors = data.map((dataset) => {
  202. return {
  203. name: dataset.Factor,
  204. weight: this.initialWeights[dataset.Factor] ?? 1,
  205. datasets: [],
  206. };
  207. });
  208. this.factors = this.hsUtilsService.removeDuplicates(
  209. this.factors,
  210. 'name'
  211. );
  212. this.factors.forEach((factor) => {
  213. factor.datasets = data
  214. .filter((ds) => ds.Factor === factor.name)
  215. .map((ds) => {
  216. return {
  217. name: ds.Name,
  218. desc: ds.Description,
  219. included: true,
  220. };
  221. });
  222. });
  223. this._loadInProcess = false;
  224. this.apply();
  225. // In HSL 2.5, setting layer greyscale breaks the print() functionality
  226. //this.hsLayerManagerService.setGreyscale(osmLayer);
  227. })
  228. .catch((error) => {
  229. console.warn(`Web service at ${this.serviceBaseUrl} unavailable!`);
  230. console.log(error);
  231. this._loadInProcess = false;
  232. /*this.adjusterEventService.loaded.next({
  233. success: false,
  234. err: error,
  235. });*/
  236. });
  237. }
  238. clustersLoaded(): boolean {
  239. return this._clustersLoaded;
  240. }
  241. /**
  242. * @returns {boolean} true if clustering or index processing is in process or loading data, false otherwise
  243. */
  244. isInProcess(): boolean {
  245. return (
  246. this._loadInProcess || this._clusteringInProcess || this._raiInProcess
  247. );
  248. }
  249. isLoading(): boolean {
  250. return this._loadInProcess;
  251. }
  252. isClustering(): boolean {
  253. return this._clusteringInProcess;
  254. }
  255. isCalculatingRAI(): boolean {
  256. return this._raiInProcess;
  257. }
  258. }
  259. type MethodDescription = {
  260. codename: string;
  261. layer?: VectorLayer;
  262. name: string;
  263. type: string;
  264. };