2022-08-18 18:29:11 +02:00
import { ChangeDetectionStrategy , Component , Input , Output , EventEmitter , NgZone , OnInit } from '@angular/core' ;
2022-09-21 17:23:45 +02:00
import { SeoService } from '../../services/seo.service' ;
import { ApiService } from '../../services/api.service' ;
2023-02-18 21:17:58 +09:00
import { delay , Observable , switchMap , tap , zip } from 'rxjs' ;
2022-09-21 17:23:45 +02:00
import { AssetsService } from '../../services/assets.service' ;
2022-07-23 15:43:38 +02:00
import { ActivatedRoute , ParamMap , Router } from '@angular/router' ;
2022-09-21 17:23:45 +02:00
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe' ;
import { StateService } from '../../services/state.service' ;
2023-11-06 18:19:54 +00:00
import { EChartsOption , echarts } from '../../graphs/echarts' ;
2022-09-21 17:23:45 +02:00
import { isMobile } from '../../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
2023-08-30 20:26:07 +09: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' ,
2023-08-30 20:26:07 +09:00
} ;
2022-07-21 22:43:12 +02:00
constructor (
private seoService : SeoService ,
private apiService : ApiService ,
2023-11-02 01:29:55 +00:00
public stateService : StateService ,
2022-07-21 22:43:12 +02:00
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 ] ;
}
2023-08-30 20:26:07 +09:00
2022-07-23 15:43:38 +02:00
if ( this . style === 'graph' ) {
2023-11-24 18:14:16 +09:00
this . center = [ 0 , 5 ] ;
2022-10-07 00:54:33 +04:00
this . seoService . setTitle ( $localize ` Lightning Nodes Channels World Map ` ) ;
2023-08-30 20:26:07 +09:00
this . seoService . setDescription ( $localize ` :@@meta.description.lightning.node-map:See the channels of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details. ` ) ;
2022-07-23 15:43:38 +02:00
}
2022-08-18 15:25:11 +02:00
if ( [ 'nodepage' , 'channelpage' ] . includes ( this . style ) ) {
this . nodeSize = 8 ;
}
2023-08-30 20:26:07 +09:00
2022-08-18 18:29:11 +02:00
this . channelsObservable = this . activatedRoute . paramMap
2022-07-23 15:43:38 +02:00
. pipe (
2023-02-18 21:17:58 +09:00
delay ( 100 ) ,
2022-07-23 15:43:38 +02:00
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 ;
}
2023-08-30 20:26:07 +09:00
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 ) = > {
2023-11-06 18:19:54 +00:00
echarts . 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 ;
2023-08-30 20:26:07 +09:00
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 = [ ] ;
2023-08-30 20:26:07 +09:00
channelLoc . push ( nodesPubkeys [ node1UniqueId ] . slice ( 0 , 2 ) ) ;
2022-08-22 17:55:19 +02:00
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 ;
2023-03-22 13:20:22 +09:00
if ( channels . length === 0 ) {
if ( ! this . placeholder ) {
this . isLoading = false ;
title = {
textStyle : {
color : 'white' ,
fontSize : 18
} ,
text : $localize ` No data to display yet. Try again later. ` ,
left : 'center' ,
top : 'center'
} ;
this . zoom = 1.5 ;
this . center = [ 0 , 20 ] ;
} else { // used for Node and Channel preview components
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-08-27 19:02:22 +00:00
}
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 ;
} ) ;
2023-08-30 20:26:07 +09:00
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 ] ) ;
} ) ;
} ) ;
}
2023-08-30 20:26:07 +09: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
}