2022-08-18 18:29:11 +02:00
|
|
|
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter, NgZone, OnInit } from '@angular/core';
|
2022-07-21 22:43:12 +02:00
|
|
|
import { SeoService } from 'src/app/services/seo.service';
|
|
|
|
import { ApiService } from 'src/app/services/api.service';
|
2022-07-23 15:43:38 +02:00
|
|
|
import { Observable, switchMap, tap, zip } from 'rxjs';
|
2022-07-21 22:43:12 +02:00
|
|
|
import { AssetsService } from 'src/app/services/assets.service';
|
2022-07-23 15:43:38 +02:00
|
|
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
2022-07-21 22:43:12 +02:00
|
|
|
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
|
|
|
|
import { StateService } from 'src/app/services/state.service';
|
|
|
|
import { EChartsOption, registerMap } from 'echarts';
|
|
|
|
import 'echarts-gl';
|
2022-08-11 10:19:13 +02:00
|
|
|
import { isMobile } from 'src/app/shared/common.utils';
|
2022-07-21 22:43:12 +02:00
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'app-nodes-channels-map',
|
|
|
|
templateUrl: './nodes-channels-map.component.html',
|
|
|
|
styleUrls: ['./nodes-channels-map.component.scss'],
|
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
|
})
|
2022-08-18 18:29:11 +02:00
|
|
|
export class NodesChannelsMap implements OnInit {
|
2022-08-10 16:00:12 +02:00
|
|
|
@Input() style: 'graph' | 'nodepage' | 'widget' | 'channelpage' = 'graph';
|
2022-07-23 15:43:38 +02:00
|
|
|
@Input() publicKey: string | undefined;
|
2022-08-10 16:00:12 +02:00
|
|
|
@Input() channel: any[] = [];
|
2022-08-11 17:19:12 +00:00
|
|
|
@Input() fitContainer = false;
|
2022-08-23 17:52:37 +02:00
|
|
|
@Input() hasLocation = true;
|
2022-08-27 19:02:22 +00:00
|
|
|
@Input() placeholder = false;
|
2022-09-09 19:01:32 +00:00
|
|
|
@Input() disableSpinner = false;
|
2022-08-11 17:19:12 +00:00
|
|
|
@Output() readyEvent = new EventEmitter();
|
2022-07-23 15:43:38 +02:00
|
|
|
|
2022-08-18 18:29:11 +02:00
|
|
|
channelsObservable: Observable<any>;
|
2022-08-11 17:19:12 +00:00
|
|
|
|
2022-08-10 11:28:54 +02:00
|
|
|
center: number[] | undefined;
|
|
|
|
zoom: number | undefined;
|
|
|
|
channelWidth = 0.6;
|
|
|
|
channelOpacity = 0.1;
|
2022-08-10 16:00:12 +02:00
|
|
|
channelColor = '#466d9d';
|
|
|
|
channelCurve = 0;
|
2022-08-18 15:25:11 +02:00
|
|
|
nodeSize = 4;
|
2022-08-23 17:52:37 +02:00
|
|
|
isLoading = false;
|
2022-07-21 22:43:12 +02:00
|
|
|
|
|
|
|
chartInstance = undefined;
|
2022-07-23 14:23:47 +02:00
|
|
|
chartOptions: EChartsOption = {};
|
2022-07-21 22:43:12 +02:00
|
|
|
chartInitOptions = {
|
|
|
|
renderer: 'canvas',
|
2022-07-28 07:45:37 +02:00
|
|
|
};
|
2022-07-21 22:43:12 +02:00
|
|
|
|
|
|
|
constructor(
|
|
|
|
private seoService: SeoService,
|
|
|
|
private apiService: ApiService,
|
|
|
|
private stateService: StateService,
|
|
|
|
private assetsService: AssetsService,
|
|
|
|
private router: Router,
|
|
|
|
private zone: NgZone,
|
2022-07-23 15:43:38 +02:00
|
|
|
private activatedRoute: ActivatedRoute,
|
2022-07-21 22:43:12 +02:00
|
|
|
) {
|
|
|
|
}
|
|
|
|
|
|
|
|
ngOnInit(): void {
|
2022-08-10 11:28:54 +02:00
|
|
|
this.center = this.style === 'widget' ? [0, 40] : [0, 5];
|
2022-08-11 10:19:13 +02:00
|
|
|
this.zoom = 1.3;
|
|
|
|
if (this.style === 'widget' && !isMobile()) {
|
|
|
|
this.zoom = 3.5;
|
|
|
|
}
|
|
|
|
if (this.style === 'widget' && isMobile()) {
|
|
|
|
this.zoom = 1.4;
|
|
|
|
this.center = [0, 10];
|
|
|
|
}
|
|
|
|
|
2022-07-23 15:43:38 +02:00
|
|
|
if (this.style === 'graph') {
|
|
|
|
this.seoService.setTitle($localize`Lightning nodes channels world map`);
|
|
|
|
}
|
2022-08-18 15:25:11 +02:00
|
|
|
|
|
|
|
if (['nodepage', 'channelpage'].includes(this.style)) {
|
|
|
|
this.nodeSize = 8;
|
|
|
|
}
|
2022-07-23 15:43:38 +02:00
|
|
|
|
2022-08-18 18:29:11 +02:00
|
|
|
this.channelsObservable = this.activatedRoute.paramMap
|
2022-07-23 15:43:38 +02:00
|
|
|
.pipe(
|
|
|
|
switchMap((params: ParamMap) => {
|
2022-08-23 17:52:37 +02:00
|
|
|
this.isLoading = true;
|
|
|
|
if (this.style === 'channelpage' && this.channel.length === 0 || !this.hasLocation) {
|
|
|
|
this.isLoading = false;
|
|
|
|
}
|
|
|
|
|
2022-07-23 15:43:38 +02:00
|
|
|
return zip(
|
|
|
|
this.assetsService.getWorldMapJson$,
|
2022-08-22 17:55:19 +02:00
|
|
|
this.style !== 'channelpage' ? this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined, this.style) : [''],
|
2022-08-05 10:11:29 +02:00
|
|
|
[params.get('public_key') ?? undefined]
|
2022-07-23 15:43:38 +02:00
|
|
|
).pipe(tap((data) => {
|
|
|
|
registerMap('world', data[0]);
|
2022-07-21 22:43:12 +02:00
|
|
|
|
2022-07-23 15:43:38 +02:00
|
|
|
const channelsLoc = [];
|
|
|
|
const nodes = [];
|
2022-07-24 15:08:48 +02:00
|
|
|
const nodesPubkeys = {};
|
2022-08-05 10:11:29 +02:00
|
|
|
let thisNodeGPS: number[] | undefined = undefined;
|
2022-08-10 16:00:12 +02:00
|
|
|
|
|
|
|
let geoloc = data[1];
|
|
|
|
if (this.style === 'channelpage') {
|
|
|
|
if (this.channel.length === 0) {
|
|
|
|
geoloc = [];
|
|
|
|
} else {
|
|
|
|
geoloc = [this.channel];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const channel of geoloc) {
|
2022-08-22 17:55:19 +02:00
|
|
|
if (this.style === 'nodepage' && !thisNodeGPS) {
|
|
|
|
if (data[2] === channel[0]) {
|
|
|
|
thisNodeGPS = [channel[2], channel[3]];
|
|
|
|
} else if (data[2] === channel[4]) {
|
|
|
|
thisNodeGPS = [channel[6], channel[7]];
|
|
|
|
}
|
2022-08-05 10:11:29 +02:00
|
|
|
}
|
|
|
|
|
2022-08-10 16:00:12 +02:00
|
|
|
// 0 - node1 pubkey
|
|
|
|
// 1 - node1 alias
|
|
|
|
// 2,3 - node1 GPS
|
|
|
|
// 4 - node2 pubkey
|
|
|
|
// 5 - node2 alias
|
|
|
|
// 6,7 - node2 GPS
|
2022-08-22 17:55:19 +02:00
|
|
|
const node1PubKey = 0;
|
|
|
|
const node1Alias = 1;
|
|
|
|
let node1GpsLat = 2;
|
|
|
|
let node1GpsLgt = 3;
|
|
|
|
const node2PubKey = 4;
|
|
|
|
const node2Alias = 5;
|
|
|
|
let node2GpsLat = 6;
|
|
|
|
let node2GpsLgt = 7;
|
|
|
|
let node1UniqueId = channel[node1PubKey];
|
|
|
|
let node2UniqueId = channel[node2PubKey];
|
|
|
|
if (this.style === 'widget') {
|
|
|
|
node1GpsLat = 0;
|
|
|
|
node1GpsLgt = 1;
|
|
|
|
node2GpsLat = 2;
|
|
|
|
node2GpsLgt = 3;
|
|
|
|
node1UniqueId = channel[node1GpsLat].toString() + channel[node1GpsLgt].toString();
|
|
|
|
node2UniqueId = channel[node2GpsLat].toString() + channel[node2GpsLgt].toString();
|
|
|
|
}
|
2022-08-10 16:00:12 +02:00
|
|
|
|
2022-08-10 11:28:54 +02:00
|
|
|
// We add a bit of noise so nodes at the same location are not all
|
|
|
|
// on top of each other
|
|
|
|
let random = Math.random() * 2 * Math.PI;
|
|
|
|
let random2 = Math.random() * 0.01;
|
|
|
|
|
2022-08-22 17:55:19 +02:00
|
|
|
if (!nodesPubkeys[node1UniqueId]) {
|
2022-08-10 11:28:54 +02:00
|
|
|
nodes.push([
|
2022-08-22 17:55:19 +02:00
|
|
|
channel[node1GpsLat] + random2 * Math.cos(random),
|
|
|
|
channel[node1GpsLgt] + random2 * Math.sin(random),
|
2022-08-10 11:28:54 +02:00
|
|
|
1,
|
2022-08-22 17:55:19 +02:00
|
|
|
channel[node1PubKey],
|
|
|
|
channel[node1Alias]
|
2022-08-10 11:28:54 +02:00
|
|
|
]);
|
2022-08-22 17:55:19 +02:00
|
|
|
nodesPubkeys[node1UniqueId] = nodes[nodes.length - 1];
|
2022-07-24 15:08:48 +02:00
|
|
|
}
|
2022-08-10 11:28:54 +02:00
|
|
|
|
|
|
|
random = Math.random() * 2 * Math.PI;
|
|
|
|
random2 = Math.random() * 0.01;
|
|
|
|
|
2022-08-22 17:55:19 +02:00
|
|
|
if (!nodesPubkeys[node2UniqueId]) {
|
2022-08-10 11:28:54 +02:00
|
|
|
nodes.push([
|
2022-08-22 17:55:19 +02:00
|
|
|
channel[node2GpsLat] + random2 * Math.cos(random),
|
|
|
|
channel[node2GpsLgt] + random2 * Math.sin(random),
|
2022-08-10 11:28:54 +02:00
|
|
|
1,
|
2022-08-22 17:55:19 +02:00
|
|
|
channel[node2PubKey],
|
|
|
|
channel[node2Alias]
|
2022-08-10 11:28:54 +02:00
|
|
|
]);
|
2022-08-22 17:55:19 +02:00
|
|
|
nodesPubkeys[node2UniqueId] = nodes[nodes.length - 1];
|
2022-07-24 15:08:48 +02:00
|
|
|
}
|
2022-08-10 11:28:54 +02:00
|
|
|
|
|
|
|
const channelLoc = [];
|
2022-08-22 17:55:19 +02:00
|
|
|
channelLoc.push(nodesPubkeys[node1UniqueId].slice(0, 2));
|
|
|
|
channelLoc.push(nodesPubkeys[node2UniqueId].slice(0, 2));
|
2022-08-10 11:28:54 +02:00
|
|
|
channelsLoc.push(channelLoc);
|
2022-07-23 15:43:38 +02:00
|
|
|
}
|
2022-08-22 17:55:19 +02:00
|
|
|
|
2022-08-05 10:11:29 +02:00
|
|
|
if (this.style === 'nodepage' && thisNodeGPS) {
|
2022-08-10 11:28:54 +02:00
|
|
|
this.center = [thisNodeGPS[0], thisNodeGPS[1]];
|
2022-08-23 16:26:01 +02:00
|
|
|
this.zoom = 5;
|
2022-08-10 11:28:54 +02:00
|
|
|
this.channelWidth = 1;
|
|
|
|
this.channelOpacity = 1;
|
2022-08-05 10:11:29 +02:00
|
|
|
}
|
2022-08-22 17:55:19 +02:00
|
|
|
|
2022-08-10 16:00:12 +02:00
|
|
|
if (this.style === 'channelpage' && this.channel.length > 0) {
|
|
|
|
this.channelWidth = 2;
|
|
|
|
this.channelOpacity = 1;
|
|
|
|
this.channelColor = '#bafcff';
|
|
|
|
this.channelCurve = 0.1;
|
|
|
|
this.center = [
|
|
|
|
(this.channel[2] + this.channel[6]) / 2,
|
|
|
|
(this.channel[3] + this.channel[7]) / 2
|
|
|
|
];
|
|
|
|
const distance = Math.sqrt(
|
|
|
|
Math.pow(this.channel[7] - this.channel[3], 2) +
|
|
|
|
Math.pow(this.channel[6] - this.channel[2], 2)
|
|
|
|
);
|
|
|
|
|
|
|
|
this.zoom = -0.05 * distance + 8;
|
|
|
|
}
|
2022-08-05 10:11:29 +02:00
|
|
|
|
2022-07-23 15:43:38 +02:00
|
|
|
this.prepareChartOptions(nodes, channelsLoc);
|
|
|
|
}));
|
|
|
|
})
|
|
|
|
);
|
2022-07-21 22:43:12 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
prepareChartOptions(nodes, channels) {
|
|
|
|
let title: object;
|
2022-08-27 19:02:22 +00:00
|
|
|
if (channels.length === 0 && !this.placeholder) {
|
2022-08-18 18:29:11 +02:00
|
|
|
this.chartOptions = null;
|
|
|
|
return;
|
2022-07-21 22:43:12 +02:00
|
|
|
}
|
|
|
|
|
2022-08-27 19:02:22 +00:00
|
|
|
// empty map fallback
|
|
|
|
if (channels.length === 0 && this.placeholder) {
|
|
|
|
title = {
|
|
|
|
textStyle: {
|
|
|
|
color: 'white',
|
|
|
|
fontSize: 18
|
|
|
|
},
|
|
|
|
text: $localize`No geolocation data available`,
|
|
|
|
left: 'center',
|
|
|
|
top: 'center'
|
|
|
|
};
|
|
|
|
this.zoom = 1.5;
|
|
|
|
this.center = [0, 20];
|
|
|
|
}
|
|
|
|
|
2022-07-21 22:43:12 +02:00
|
|
|
this.chartOptions = {
|
2022-08-10 11:28:54 +02:00
|
|
|
silent: this.style === 'widget',
|
2022-07-23 15:43:38 +02:00
|
|
|
title: title ?? undefined,
|
2022-08-10 11:28:54 +02:00
|
|
|
tooltip: {},
|
|
|
|
geo: {
|
|
|
|
animation: false,
|
2022-07-21 22:43:12 +02:00
|
|
|
silent: true,
|
2022-08-10 11:28:54 +02:00
|
|
|
center: this.center,
|
|
|
|
zoom: this.zoom,
|
|
|
|
tooltip: {
|
2022-08-10 16:42:01 +02:00
|
|
|
show: false
|
2022-07-21 22:43:12 +02:00
|
|
|
},
|
2022-08-10 11:28:54 +02:00
|
|
|
map: 'world',
|
|
|
|
roam: this.style === 'widget' ? false : true,
|
2022-07-21 22:43:12 +02:00
|
|
|
itemStyle: {
|
2022-07-24 15:08:48 +02:00
|
|
|
borderColor: 'black',
|
2022-08-30 08:09:14 +02:00
|
|
|
color: '#272b3f'
|
2022-07-21 22:43:12 +02:00
|
|
|
},
|
2022-08-10 11:28:54 +02:00
|
|
|
scaleLimit: {
|
|
|
|
min: 1.3,
|
|
|
|
max: 100000,
|
2022-08-10 16:42:01 +02:00
|
|
|
},
|
|
|
|
emphasis: {
|
|
|
|
disabled: true,
|
2022-08-10 11:28:54 +02:00
|
|
|
}
|
2022-07-21 22:43:12 +02:00
|
|
|
},
|
|
|
|
series: [
|
|
|
|
{
|
2022-08-10 11:28:54 +02:00
|
|
|
large: true,
|
|
|
|
type: 'scatter',
|
|
|
|
data: nodes,
|
|
|
|
coordinateSystem: 'geo',
|
|
|
|
geoIndex: 0,
|
2022-08-18 15:25:11 +02:00
|
|
|
symbolSize: this.nodeSize,
|
2022-08-10 11:28:54 +02:00
|
|
|
tooltip: {
|
2022-08-10 16:42:01 +02:00
|
|
|
show: true,
|
2022-08-10 11:28:54 +02:00
|
|
|
backgroundColor: 'rgba(17, 19, 31, 1)',
|
|
|
|
borderRadius: 4,
|
|
|
|
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
|
|
|
textStyle: {
|
|
|
|
color: '#b1b1b1',
|
|
|
|
align: 'left',
|
|
|
|
},
|
|
|
|
borderColor: '#000',
|
|
|
|
formatter: (value) => {
|
|
|
|
const data = value.data;
|
|
|
|
const alias = data[4].length > 0 ? data[4] : data[3].slice(0, 20);
|
|
|
|
return `<b style="color: white">${alias}</b>`;
|
|
|
|
}
|
2022-07-21 22:43:12 +02:00
|
|
|
},
|
|
|
|
itemStyle: {
|
2022-08-10 11:28:54 +02:00
|
|
|
color: 'white',
|
2022-07-21 22:43:12 +02:00
|
|
|
opacity: 1,
|
2022-08-10 16:42:01 +02:00
|
|
|
borderColor: 'black',
|
|
|
|
borderWidth: 0,
|
2022-07-21 22:43:12 +02:00
|
|
|
},
|
2022-08-10 11:28:54 +02:00
|
|
|
blendMode: 'lighter',
|
2022-08-10 16:42:01 +02:00
|
|
|
zlevel: 2,
|
2022-07-21 22:43:12 +02:00
|
|
|
},
|
2022-08-10 11:28:54 +02:00
|
|
|
{
|
2022-08-10 17:03:11 +02:00
|
|
|
large: false,
|
2022-08-22 17:55:19 +02:00
|
|
|
progressive: this.style === 'widget' ? 500 : 200,
|
2022-08-10 11:28:54 +02:00
|
|
|
silent: true,
|
|
|
|
type: 'lines',
|
|
|
|
coordinateSystem: 'geo',
|
|
|
|
data: channels,
|
|
|
|
lineStyle: {
|
|
|
|
opacity: this.channelOpacity,
|
|
|
|
width: this.channelWidth,
|
2022-08-10 16:00:12 +02:00
|
|
|
curveness: this.channelCurve,
|
|
|
|
color: this.channelColor,
|
2022-08-10 11:28:54 +02:00
|
|
|
},
|
|
|
|
blendMode: 'lighter',
|
|
|
|
tooltip: {
|
|
|
|
show: false,
|
|
|
|
},
|
2022-08-10 16:42:01 +02:00
|
|
|
zlevel: 1,
|
2022-08-10 11:28:54 +02:00
|
|
|
}
|
2022-07-21 22:43:12 +02:00
|
|
|
]
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
onChartInit(ec) {
|
|
|
|
if (this.chartInstance !== undefined) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.chartInstance = ec;
|
|
|
|
|
2022-08-22 17:55:19 +02:00
|
|
|
this.chartInstance.on('finished', () => {
|
|
|
|
this.isLoading = false;
|
|
|
|
});
|
|
|
|
|
2022-07-28 07:45:37 +02:00
|
|
|
if (this.style === 'widget') {
|
|
|
|
this.chartInstance.getZr().on('click', (e) => {
|
|
|
|
this.zone.run(() => {
|
|
|
|
const url = new RelativeUrlPipe(this.stateService).transform(`/graphs/lightning/nodes-channels-map`);
|
|
|
|
this.router.navigate([url]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
2022-08-10 11:28:54 +02:00
|
|
|
|
2022-07-21 22:43:12 +02:00
|
|
|
this.chartInstance.on('click', (e) => {
|
2022-08-10 11:28:54 +02:00
|
|
|
if (e.data) {
|
2022-07-21 22:43:12 +02:00
|
|
|
this.zone.run(() => {
|
2022-08-10 11:28:54 +02:00
|
|
|
const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/node/${e.data[3]}`);
|
2022-07-21 22:43:12 +02:00
|
|
|
this.router.navigate([url]);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
2022-08-10 11:28:54 +02:00
|
|
|
|
|
|
|
this.chartInstance.on('georoam', (e) => {
|
|
|
|
if (!e.zoom || this.style === 'nodepage') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const speed = 0.005;
|
|
|
|
const chartOptions = {
|
|
|
|
series: this.chartOptions.series
|
|
|
|
};
|
|
|
|
|
2022-08-10 16:42:01 +02:00
|
|
|
let nodeBorder = 0;
|
|
|
|
if (this.chartInstance.getOption().geo[0].zoom > 5000) {
|
|
|
|
nodeBorder = 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
chartOptions.series[0].itemStyle.borderWidth = nodeBorder;
|
|
|
|
chartOptions.series[0].symbolSize += e.zoom > 1 ? speed * 15 : -speed * 15;
|
|
|
|
chartOptions.series[0].symbolSize = Math.max(4, Math.min(7, chartOptions.series[0].symbolSize));
|
|
|
|
|
2022-08-10 11:28:54 +02:00
|
|
|
chartOptions.series[1].lineStyle.opacity += e.zoom > 1 ? speed : -speed;
|
|
|
|
chartOptions.series[1].lineStyle.width += e.zoom > 1 ? speed : -speed;
|
|
|
|
chartOptions.series[1].lineStyle.opacity = Math.max(0.05, Math.min(0.5, chartOptions.series[1].lineStyle.opacity));
|
|
|
|
chartOptions.series[1].lineStyle.width = Math.max(0.5, Math.min(1, chartOptions.series[1].lineStyle.width));
|
|
|
|
|
|
|
|
this.chartInstance.setOption(chartOptions);
|
|
|
|
});
|
2022-07-21 22:43:12 +02:00
|
|
|
}
|
2022-08-11 17:19:12 +00:00
|
|
|
|
|
|
|
onChartFinished(e) {
|
|
|
|
this.readyEvent.emit();
|
|
|
|
}
|
2022-07-21 22:43:12 +02:00
|
|
|
}
|