2022-09-10 14:53:52 +00:00
import { ChangeDetectionStrategy , Component , Inject , Input , Output , EventEmitter , LOCALE_ID , NgZone , OnDestroy , OnInit , OnChanges } from '@angular/core' ;
2022-09-21 17:23:45 +02:00
import { SeoService } from '../../services/seo.service' ;
import { ApiService } from '../../services/api.service' ;
2022-09-10 14:53:52 +00:00
import { Observable , BehaviorSubject , switchMap , tap , combineLatest } from 'rxjs' ;
2022-09-21 17:23:45 +02:00
import { AssetsService } from '../../services/assets.service' ;
2022-07-21 22:43:12 +02:00
import { EChartsOption , registerMap } from 'echarts' ;
2022-09-21 17:23:45 +02:00
import { lerpColor } from '../../shared/graphs.utils' ;
2022-07-20 11:39:51 +02:00
import { 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' ;
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe' ;
import { getFlagEmoji } from '../../shared/common.utils' ;
2022-07-20 11:39:51 +02:00
@Component ( {
selector : 'app-nodes-map' ,
templateUrl : './nodes-map.component.html' ,
styleUrls : [ './nodes-map.component.scss' ] ,
changeDetection : ChangeDetectionStrategy.OnPush ,
} )
2022-09-10 14:53:52 +00:00
export class NodesMap implements OnInit , OnChanges {
2022-09-09 14:56:18 +02:00
@Input ( ) widget : boolean = false ;
@Input ( ) nodes : any [ ] | undefined = undefined ;
@Input ( ) type : 'none' | 'isp' | 'country' = 'none' ;
2022-09-10 14:53:52 +00:00
@Input ( ) fitContainer = false ;
@Output ( ) readyEvent = new EventEmitter ( ) ;
inputNodes$ : BehaviorSubject < any > ;
nodes$ : Observable < any > ;
2022-07-20 11:39:51 +02:00
observable$ : Observable < any > ;
chartInstance = undefined ;
chartOptions : EChartsOption = { } ;
chartInitOptions = {
renderer : 'svg' ,
} ;
constructor (
2022-09-06 19:33:07 +02:00
@Inject ( LOCALE_ID ) public locale : string ,
2022-07-20 11:39:51 +02:00
private seoService : SeoService ,
private apiService : ApiService ,
private stateService : StateService ,
private assetsService : AssetsService ,
private router : Router ,
private zone : NgZone ,
2022-09-06 19:33:07 +02:00
private amountShortenerPipe : AmountShortenerPipe
2022-07-20 11:39:51 +02:00
) {
}
ngOnInit ( ) : void {
2022-09-17 01:26:32 +02:00
if ( ! this . widget ) {
2022-10-13 17:15:17 +04:00
this . seoService . setTitle ( $localize ` :@@af8560ca50882114be16c951650f83bca73161a7:Lightning Nodes World Map ` ) ;
2023-08-30 20:26:07 +09:00
this . seoService . setDescription ( $localize ` :@@meta.description.lightning.node-channel-map:See the locations of non-Tor Lightning network nodes visualized on a world map. Hover/tap on points on the map for node names and details. ` ) ;
2022-09-17 01:26:32 +02:00
}
2022-07-20 11:39:51 +02:00
2022-09-10 14:53:52 +00:00
if ( ! this . inputNodes $ ) {
this . inputNodes $ = new BehaviorSubject ( this . nodes ) ;
}
this . nodes $ = this . inputNodes $ . pipe (
switchMap ( ( nodes ) = > nodes ? [ nodes ] : this . apiService . getWorldNodes $ ( ) )
) ;
this . observable $ = combineLatest (
2022-07-20 11:39:51 +02:00
this . assetsService . getWorldMapJson $ ,
2022-09-10 14:53:52 +00:00
this . nodes $
2022-09-06 19:33:07 +02:00
) . pipe ( tap ( ( data ) = > {
2022-07-20 11:39:51 +02:00
registerMap ( 'world' , data [ 0 ] ) ;
2022-09-09 14:56:18 +02:00
let maxLiquidity = data [ 1 ] . maxLiquidity ;
let inputNodes : any [ ] = data [ 1 ] . nodes ;
let mapCenter : number [ ] = [ 0 , 5 ] ;
if ( this . type === 'country' ) {
mapCenter = [ 0 , 0 ] ;
} else if ( this . type === 'isp' ) {
mapCenter = [ 0 , 10 ] ;
}
let mapZoom = 1.3 ;
if ( ! inputNodes ) {
inputNodes = [ ] ;
for ( const node of data [ 1 ] ) {
if ( this . type === 'country' ) {
mapCenter [ 0 ] += node . longitude ;
mapCenter [ 1 ] += node . latitude ;
}
inputNodes . push ( [
node . longitude ,
node . latitude ,
node . public_key ,
node . alias ,
node . capacity ,
node . channels ,
node . country ,
node . iso_code ,
] ) ;
maxLiquidity = Math . max ( maxLiquidity ? ? 0 , node . capacity ) ;
}
if ( this . type === 'country' ) {
mapCenter [ 0 ] /= data [ 1 ] . length ;
mapCenter [ 1 ] /= data [ 1 ] . length ;
mapZoom = 6 ;
}
}
2022-09-06 19:33:07 +02:00
const nodes : any [ ] = [ ] ;
2022-09-09 14:56:18 +02:00
for ( const node of inputNodes ) {
2022-09-06 19:33:07 +02:00
// We add a bit of noise so nodes at the same location are not all
// on top of each other
const random = Math . random ( ) * 2 * Math . PI ;
const random2 = Math . random ( ) * 0.01 ;
nodes . push ( [
node [ 0 ] + random2 * Math . cos ( random ) ,
node [ 1 ] + random2 * Math . sin ( random ) ,
node [ 4 ] , // Liquidity
node [ 3 ] , // Alias
node [ 2 ] , // Public key
node [ 5 ] , // Channels
node [ 6 ] . en , // Country
node [ 7 ] , // ISO Code
] ) ;
2022-07-20 11:39:51 +02:00
}
2022-09-09 14:56:18 +02:00
maxLiquidity = Math . max ( 1 , maxLiquidity ) ;
this . prepareChartOptions ( nodes , maxLiquidity , mapCenter , mapZoom ) ;
2022-07-20 11:39:51 +02:00
} ) ) ;
}
2022-09-10 14:53:52 +00:00
ngOnChanges ( changes ) : void {
if ( changes . nodes ) {
if ( ! this . inputNodes $ ) {
this . inputNodes $ = new BehaviorSubject ( changes . nodes . currentValue ) ;
} else {
this . inputNodes $ . next ( changes . nodes . currentValue ) ;
}
}
}
2022-09-09 14:56:18 +02:00
prepareChartOptions ( nodes , maxLiquidity , mapCenter , mapZoom ) {
2022-07-20 11:39:51 +02:00
let title : object ;
2022-09-06 19:33:07 +02:00
if ( nodes . length === 0 ) {
2022-07-20 11:39:51 +02:00
title = {
textStyle : {
color : 'grey' ,
fontSize : 15
} ,
2022-10-07 00:54:33 +04:00
text : $localize ` No data to display yet. Try again later. ` ,
2022-07-20 11:39:51 +02:00
left : 'center' ,
top : 'center'
} ;
}
this . chartOptions = {
2022-09-06 19:33:07 +02:00
silent : false ,
title : title ? ? undefined ,
tooltip : { } ,
geo : {
animation : false ,
silent : true ,
2022-09-09 14:56:18 +02:00
center : mapCenter ,
zoom : mapZoom ,
2022-09-06 19:33:07 +02:00
tooltip : {
show : false
2022-07-20 11:39:51 +02:00
} ,
2022-09-06 19:33:07 +02:00
map : 'world' ,
roam : true ,
itemStyle : {
borderColor : 'black' ,
color : '#272b3f'
2022-07-20 11:39:51 +02:00
} ,
2022-09-06 19:33:07 +02:00
scaleLimit : {
min : 1.3 ,
max : 100000 ,
2022-07-20 11:39:51 +02:00
} ,
emphasis : {
2022-09-06 19:33:07 +02:00
disabled : true ,
}
} ,
series : [
{
large : false ,
type : 'scatter' ,
data : nodes ,
coordinateSystem : 'geo' ,
geoIndex : 0 ,
progressive : 500 ,
symbolSize : function ( params ) {
return 10 * Math . pow ( params [ 2 ] / maxLiquidity , 0.2 ) + 3 ;
} ,
tooltip : {
2022-09-06 21:17:15 +02:00
position : function ( point , params , dom , rect , size ) {
return point ;
} ,
2022-09-06 19:33:07 +02:00
trigger : 'item' ,
show : true ,
backgroundColor : 'rgba(17, 19, 31, 1)' ,
2022-09-06 21:17:15 +02:00
borderRadius : 0 ,
2022-09-06 19:33:07 +02:00
shadowColor : 'rgba(0, 0, 0, 0.5)' ,
textStyle : {
color : '#b1b1b1' ,
align : 'left' ,
} ,
borderColor : '#000' ,
formatter : ( value ) = > {
const data = value . data ;
const alias = data [ 3 ] . length > 0 ? data [ 3 ] : data [ 4 ] . slice ( 0 , 20 ) ;
const liquidity = data [ 2 ] >= 100000000 ?
` ${ this . amountShortenerPipe . transform ( data [ 2 ] / 100000000 ) } BTC ` :
` ${ this . amountShortenerPipe . transform ( data [ 2 ] , 2 ) } sats ` ;
return `
< b style = "color: white" > $ { alias } < / b > < br >
2023-03-13 18:42:43 +09:00
$ { liquidity } < br > ` +
$localize ` :@@205c1b86ac1cc419c4d0cca51fdde418c4ffdc20: ${ data [ 5 ] } :INTERPOLATION: channels ` + ` <br>
2022-09-06 19:33:07 +02:00
$ { getFlagEmoji ( data [ 7 ] ) } $ { data [ 6 ] }
` ;
}
2022-07-20 11:39:51 +02:00
} ,
itemStyle : {
2022-09-06 19:33:07 +02:00
color : function ( params ) {
return ` ${ lerpColor ( '#1E88E5' , '#D81B60' , Math . pow ( params . data [ 2 ] / maxLiquidity , 0.2 ) ) } ` ;
} ,
opacity : 1 ,
borderColor : 'black' ,
borderWidth : 0 ,
} ,
zlevel : 2 ,
2022-07-20 11:39:51 +02:00
} ,
2022-09-06 19:33:07 +02:00
]
2022-07-20 11:39:51 +02:00
} ;
}
onChartInit ( ec ) {
if ( this . chartInstance !== undefined ) {
return ;
}
this . chartInstance = ec ;
this . chartInstance . on ( 'click' , ( e ) = > {
2022-09-06 19:33:07 +02:00
if ( e . data ) {
2022-07-20 11:39:51 +02:00
this . zone . run ( ( ) = > {
2022-09-06 19:33:07 +02:00
const url = new RelativeUrlPipe ( this . stateService ) . transform ( ` /lightning/node/ ${ e . data [ 4 ] } ` ) ;
2022-07-20 11:39:51 +02:00
this . router . navigate ( [ url ] ) ;
} ) ;
}
} ) ;
2022-09-06 19:33:07 +02:00
this . chartInstance . on ( 'georoam' , ( e ) = > {
this . chartInstance . resize ( ) ;
} ) ;
2022-07-20 11:39:51 +02:00
}
2022-09-10 14:53:52 +00:00
onChartFinished ( e ) {
this . readyEvent . emit ( ) ;
}
2022-07-20 11:39:51 +02:00
}