adjuster.service.ts 13 KB

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