Merge branch 'master' into simon/node-alias-fulltext-search
This commit is contained in:
		
						commit
						4727b4bade
					
				@ -77,13 +77,19 @@
 | 
			
		||||
  },
 | 
			
		||||
  "LIGHTNING": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "BACKEND": "lnd"
 | 
			
		||||
    "BACKEND": "lnd",
 | 
			
		||||
    "STATS_REFRESH_INTERVAL": 600,
 | 
			
		||||
    "GRAPH_REFRESH_INTERVAL": 600,
 | 
			
		||||
    "LOGGER_UPDATE_INTERVAL": 30
 | 
			
		||||
  },
 | 
			
		||||
  "LND": {
 | 
			
		||||
    "TLS_CERT_PATH": "tls.cert",
 | 
			
		||||
    "MACAROON_PATH": "readonly.macaroon",
 | 
			
		||||
    "REST_API_URL": "https://localhost:8080"
 | 
			
		||||
  },
 | 
			
		||||
  "CLIGHTNING": {
 | 
			
		||||
    "SOCKET": "lightning-rpc"
 | 
			
		||||
  },
 | 
			
		||||
  "SOCKS5PROXY": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "USE_ONION": true,
 | 
			
		||||
 | 
			
		||||
@ -88,5 +88,21 @@
 | 
			
		||||
    "LIQUID_ONION": "__EXTERNAL_DATA_SERVER_LIQUID_ONION__",
 | 
			
		||||
    "BISQ_URL": "__EXTERNAL_DATA_SERVER_BISQ_URL__",
 | 
			
		||||
    "BISQ_ONION": "__EXTERNAL_DATA_SERVER_BISQ_ONION__"
 | 
			
		||||
  },
 | 
			
		||||
  "LIGHTNING": {
 | 
			
		||||
    "ENABLED": "__LIGHTNING_ENABLED__",
 | 
			
		||||
    "BACKEND": "__LIGHTNING_BACKEND__",
 | 
			
		||||
    "TOPOLOGY_FOLDER": "__LIGHTNING_TOPOLOGY_FOLDER__",
 | 
			
		||||
    "STATS_REFRESH_INTERVAL": 600,
 | 
			
		||||
    "GRAPH_REFRESH_INTERVAL": 600,
 | 
			
		||||
    "LOGGER_UPDATE_INTERVAL": 30
 | 
			
		||||
  },
 | 
			
		||||
  "LND": {
 | 
			
		||||
    "TLS_CERT_PATH": "",
 | 
			
		||||
    "MACAROON_PATH": "",
 | 
			
		||||
    "REST_API_URL": "https://localhost:8080"
 | 
			
		||||
  },
 | 
			
		||||
  "CLIGHTNING": {
 | 
			
		||||
    "SOCKET": "__CLIGHTNING_SOCKET__"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -297,19 +297,24 @@ class NodesApi {
 | 
			
		||||
 | 
			
		||||
        if (!ispList[isp1]) {
 | 
			
		||||
          ispList[isp1] = {
 | 
			
		||||
            id: channel.isp1ID,
 | 
			
		||||
            id: channel.isp1ID.toString(),
 | 
			
		||||
            capacity: 0,
 | 
			
		||||
            channels: 0,
 | 
			
		||||
            nodes: {},
 | 
			
		||||
          };
 | 
			
		||||
        } else if (ispList[isp1].id.indexOf(channel.isp1ID) === -1) {
 | 
			
		||||
          ispList[isp1].id += ',' + channel.isp1ID.toString();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!ispList[isp2]) {
 | 
			
		||||
          ispList[isp2] = {
 | 
			
		||||
            id: channel.isp2ID,
 | 
			
		||||
            id: channel.isp2ID.toString(),
 | 
			
		||||
            capacity: 0,
 | 
			
		||||
            channels: 0,
 | 
			
		||||
            nodes: {},
 | 
			
		||||
          };
 | 
			
		||||
        } else if (ispList[isp2].id.indexOf(channel.isp2ID) === -1) {
 | 
			
		||||
          ispList[isp2].id += ',' + channel.isp2ID.toString();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        ispList[isp1].capacity += channel.capacity;
 | 
			
		||||
@ -386,9 +391,10 @@ class NodesApi {
 | 
			
		||||
  public async $getNodesPerCountry(countryId: string) {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `
 | 
			
		||||
      SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
 | 
			
		||||
      nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
 | 
			
		||||
          geo_names_city.names as city
 | 
			
		||||
        SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
 | 
			
		||||
          nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
 | 
			
		||||
          geo_names_city.names as city, geo_names_country.names as country,
 | 
			
		||||
          geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
 | 
			
		||||
        FROM node_stats
 | 
			
		||||
        JOIN (
 | 
			
		||||
          SELECT public_key, MAX(added) as last_added
 | 
			
		||||
@ -396,15 +402,19 @@ class NodesApi {
 | 
			
		||||
          GROUP BY public_key
 | 
			
		||||
        ) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added
 | 
			
		||||
        RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
 | 
			
		||||
        JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
 | 
			
		||||
        LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
 | 
			
		||||
        LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
 | 
			
		||||
        LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
 | 
			
		||||
        LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
 | 
			
		||||
        WHERE geo_names_country.id = ?
 | 
			
		||||
        ORDER BY capacity DESC
 | 
			
		||||
      `;
 | 
			
		||||
 | 
			
		||||
      const [rows]: any = await DB.query(query, [countryId]);
 | 
			
		||||
      for (let i = 0; i < rows.length; ++i) {
 | 
			
		||||
        rows[i].country = JSON.parse(rows[i].country);
 | 
			
		||||
        rows[i].city = JSON.parse(rows[i].city);
 | 
			
		||||
        rows[i].subdivision = JSON.parse(rows[i].subdivision);
 | 
			
		||||
      }
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@ -418,7 +428,8 @@ class NodesApi {
 | 
			
		||||
      const query = `
 | 
			
		||||
        SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
 | 
			
		||||
          nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
 | 
			
		||||
          geo_names_city.names as city, geo_names_country.names as country
 | 
			
		||||
          geo_names_city.names as city, geo_names_country.names as country,
 | 
			
		||||
          geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
 | 
			
		||||
        FROM node_stats
 | 
			
		||||
        JOIN (
 | 
			
		||||
          SELECT public_key, MAX(added) as last_added
 | 
			
		||||
@ -426,8 +437,10 @@ class NodesApi {
 | 
			
		||||
          GROUP BY public_key
 | 
			
		||||
        ) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added
 | 
			
		||||
        RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
 | 
			
		||||
        JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
 | 
			
		||||
        LEFT JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country'
 | 
			
		||||
        LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
 | 
			
		||||
        LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
 | 
			
		||||
        LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
 | 
			
		||||
        WHERE nodes.as_number IN (?)
 | 
			
		||||
        ORDER BY capacity DESC
 | 
			
		||||
      `;
 | 
			
		||||
@ -436,6 +449,7 @@ class NodesApi {
 | 
			
		||||
      for (let i = 0; i < rows.length; ++i) {
 | 
			
		||||
        rows[i].country = JSON.parse(rows[i].country);
 | 
			
		||||
        rows[i].city = JSON.parse(rows[i].city);
 | 
			
		||||
        rows[i].subdivision = JSON.parse(rows[i].subdivision);
 | 
			
		||||
      }
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
 | 
			
		||||
@ -36,6 +36,7 @@ interface IConfig {
 | 
			
		||||
    TOPOLOGY_FOLDER: string;
 | 
			
		||||
    STATS_REFRESH_INTERVAL: number;
 | 
			
		||||
    GRAPH_REFRESH_INTERVAL: number;
 | 
			
		||||
    LOGGER_UPDATE_INTERVAL: number;
 | 
			
		||||
  };
 | 
			
		||||
  LND: {
 | 
			
		||||
    TLS_CERT_PATH: string;
 | 
			
		||||
@ -191,6 +192,7 @@ const defaults: IConfig = {
 | 
			
		||||
    'TOPOLOGY_FOLDER': '',
 | 
			
		||||
    'STATS_REFRESH_INTERVAL': 600,
 | 
			
		||||
    'GRAPH_REFRESH_INTERVAL': 600,
 | 
			
		||||
    'LOGGER_UPDATE_INTERVAL': 30,
 | 
			
		||||
  },
 | 
			
		||||
  'LND': {
 | 
			
		||||
    'TLS_CERT_PATH': '',
 | 
			
		||||
 | 
			
		||||
@ -95,11 +95,19 @@ class NetworkSyncService {
 | 
			
		||||
   */
 | 
			
		||||
  private async $updateChannelsList(channels: ILightningApi.Channel[]): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [closedChannelsRaw]: any[] = await DB.query(`SELECT id FROM channels WHERE status = 2`);
 | 
			
		||||
      const closedChannels = {};
 | 
			
		||||
      for (const closedChannel of closedChannelsRaw) {
 | 
			
		||||
        closedChannels[Common.channelShortIdToIntegerId(closedChannel.id)] = true;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let progress = 0;
 | 
			
		||||
 | 
			
		||||
      const graphChannelsIds: string[] = [];
 | 
			
		||||
      for (const channel of channels) {
 | 
			
		||||
        await channelsApi.$saveChannel(channel);
 | 
			
		||||
        if (!closedChannels[channel.channel_id]) {
 | 
			
		||||
          await channelsApi.$saveChannel(channel);
 | 
			
		||||
        }
 | 
			
		||||
        graphChannelsIds.push(channel.channel_id);
 | 
			
		||||
        ++progress;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -350,3 +350,68 @@ Corresponding `docker-compose.yml` overrides:
 | 
			
		||||
      PRICE_DATA_SERVER_CLEARNET_URL: ""
 | 
			
		||||
      ...
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
<br/>
 | 
			
		||||
 | 
			
		||||
`mempool-config.json`:
 | 
			
		||||
```
 | 
			
		||||
  "LIGHTNING": {
 | 
			
		||||
    "ENABLED": false
 | 
			
		||||
    "BACKEND": "lnd"
 | 
			
		||||
    "TOPOLOGY_FOLDER": ""
 | 
			
		||||
    "STATS_REFRESH_INTERVAL": 600
 | 
			
		||||
    "GRAPH_REFRESH_INTERVAL": 600
 | 
			
		||||
    "LOGGER_UPDATE_INTERVAL": 30
 | 
			
		||||
  }
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Corresponding `docker-compose.yml` overrides:
 | 
			
		||||
```
 | 
			
		||||
  api:
 | 
			
		||||
    environment:
 | 
			
		||||
      LIGHTNING_ENABLED: false
 | 
			
		||||
      LIGHTNING_BACKEND: "lnd"
 | 
			
		||||
      LIGHTNING_TOPOLOGY_FOLDER: ""
 | 
			
		||||
      LIGHTNING_STATS_REFRESH_INTERVAL: 600
 | 
			
		||||
      LIGHTNING_GRAPH_REFRESH_INTERVAL: 600
 | 
			
		||||
      LIGHTNING_LOGGER_UPDATE_INTERVAL: 30
 | 
			
		||||
      ...
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
<br/>
 | 
			
		||||
 | 
			
		||||
`mempool-config.json`:
 | 
			
		||||
```
 | 
			
		||||
  "LND": {
 | 
			
		||||
    "TLS_CERT_PATH": ""
 | 
			
		||||
    "MACAROON_PATH": ""
 | 
			
		||||
    "REST_API_URL": "https://localhost:8080"
 | 
			
		||||
  }
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Corresponding `docker-compose.yml` overrides:
 | 
			
		||||
```
 | 
			
		||||
  api:
 | 
			
		||||
    environment:
 | 
			
		||||
      LND_TLS_CERT_PATH: ""
 | 
			
		||||
      LND_MACAROON_PATH: ""
 | 
			
		||||
      LND_REST_API_URL: "https://localhost:8080"
 | 
			
		||||
      ...
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
<br/>
 | 
			
		||||
 | 
			
		||||
`mempool-config.json`:
 | 
			
		||||
```
 | 
			
		||||
  "CLN": {
 | 
			
		||||
    "SOCKET": ""
 | 
			
		||||
  }
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Corresponding `docker-compose.yml` overrides:
 | 
			
		||||
```
 | 
			
		||||
  api:
 | 
			
		||||
    environment:
 | 
			
		||||
      CLN_SOCKET: ""
 | 
			
		||||
      ...
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@ -91,6 +91,22 @@ __EXTERNAL_DATA_SERVER_LIQUID_ONION__=${EXTERNAL_DATA_SERVER_LIQUID_ONION:=http:
 | 
			
		||||
__EXTERNAL_DATA_SERVER_BISQ_URL__=${EXTERNAL_DATA_SERVER_BISQ_URL:=https://bisq.markets/api}
 | 
			
		||||
__EXTERNAL_DATA_SERVER_BISQ_ONION__=${EXTERNAL_DATA_SERVER_BISQ_ONION:=http://bisqmktse2cabavbr2xjq7xw3h6g5ottemo5rolfcwt6aly6tp5fdryd.onion/api}
 | 
			
		||||
 | 
			
		||||
# LIGHTNING
 | 
			
		||||
__LIGHTNING_ENABLED__=${LIGHTNING_ENABLED:=false}
 | 
			
		||||
__LIGHTNING_BACKEND__=${LIGHTNING_BACKEND:="lnd"}
 | 
			
		||||
__LIGHTNING_TOPOLOGY_FOLDER__=${LIGHTNING_TOPOLOGY_FOLDER:=""}
 | 
			
		||||
__LIGHTNING_STATS_REFRESH_INTERVAL__=${LIGHTNING_STATS_REFRESH_INTERVAL:=600}
 | 
			
		||||
__LIGHTNING_GRAPH_REFRESH_INTERVAL__=${LIGHTNING_GRAPH_REFRESH_INTERVAL:=600}
 | 
			
		||||
__LIGHTNING_LOGGER_UPDATE_INTERVAL__=${LIGHTNING_LOGGER_UPDATE_INTERVAL:=30}
 | 
			
		||||
 | 
			
		||||
# LND
 | 
			
		||||
__LND_TLS_CERT_PATH__=${LND_TLS_CERT_PATH:=""}
 | 
			
		||||
__LND_MACAROON_PATH__=${LND_MACAROON_PATH:=""}
 | 
			
		||||
__LND_REST_API_URL__=${LND_REST_API_URL:="https://localhost:8080"}
 | 
			
		||||
 | 
			
		||||
# CLN
 | 
			
		||||
__CLN_SOCKET__=${CLN_SOCKET:=""}
 | 
			
		||||
 | 
			
		||||
mkdir -p "${__MEMPOOL_CACHE_DIR__}"
 | 
			
		||||
 | 
			
		||||
sed -i "s/__MEMPOOL_NETWORK__/${__MEMPOOL_NETWORK__}/g" mempool-config.json
 | 
			
		||||
@ -173,4 +189,20 @@ sed -i "s!__EXTERNAL_DATA_SERVER_LIQUID_ONION__!${__EXTERNAL_DATA_SERVER_LIQUID_
 | 
			
		||||
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_URL__!${__EXTERNAL_DATA_SERVER_BISQ_URL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__EXTERNAL_DATA_SERVER_BISQ_ONION__!${__EXTERNAL_DATA_SERVER_BISQ_ONION__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
# LIGHTNING
 | 
			
		||||
sed -i "s!__LIGHTNING_ENABLED__!${__LIGHTNING_ENABLED__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__LIGHTNING_BACKEND__!${__LIGHTNING_BACKEND__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__LIGHTNING_TOPOLOGY_FOLDER__!${__LIGHTNING_TOPOLOGY_FOLDER__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__LIGHTNING_STATS_REFRESH_INTERVAL__!${__LIGHTNING_STATS_REFRESH_INTERVAL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__LIGHTNING_GRAPH_REFRESH_INTERVAL__!${__LIGHTNING_GRAPH_REFRESH_INTERVAL__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__LIGHTNING_LOGGER_UPDATE_INTERVAL__!${__LIGHTNING_LOGGER_UPDATE_INTERVAL__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
# LND
 | 
			
		||||
sed -i "s!__LND_TLS_CERT_PATH__!${__LND_TLS_CERT_PATH__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__LND_MACAROON_PATH__!${__LND_MACAROON_PATH__}!g" mempool-config.json
 | 
			
		||||
sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
# CLN
 | 
			
		||||
sed -i "s!__CLN_SOCKET__!${__CLN_SOCKET__}!g" mempool-config.json
 | 
			
		||||
 | 
			
		||||
node /backend/dist/index.js
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import { Observable } from 'rxjs';
 | 
			
		||||
import { catchError, map, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { OpenGraphService } from 'src/app/services/opengraph.service';
 | 
			
		||||
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
 | 
			
		||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
 | 
			
		||||
import { LightningApiService } from '../lightning-api.service';
 | 
			
		||||
import { isMobile } from '../../shared/common.utils';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -42,24 +42,10 @@
 | 
			
		||||
                <app-fiat [value]="node.avgCapacity" digitsInfo="1.0-0"></app-fiat>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr *ngIf="node.country && node.city && node.subdivision">
 | 
			
		||||
            <tr *ngIf="node.geolocation">
 | 
			
		||||
              <td i18n="location">Location</td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <span>{{ node.city.en }}, {{ node.subdivision.en }}</span>
 | 
			
		||||
                <br>
 | 
			
		||||
                <a class="d-flex align-items-center" [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
 | 
			
		||||
                  <span class="link">{{ node.country.en }}</span>
 | 
			
		||||
                   
 | 
			
		||||
                  <span class="flag">{{ node.flag }}</span>
 | 
			
		||||
                </a>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr *ngIf="node.country && !node.city">
 | 
			
		||||
              <td i18n="location">Location</td>
 | 
			
		||||
              <td>
 | 
			
		||||
                <a [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
 | 
			
		||||
                  {{ node.country.en }} {{ node.flag }}
 | 
			
		||||
                </a>
 | 
			
		||||
                <app-geolocation [data]="node.geolocation" [type]="'node'"></app-geolocation>
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
 | 
			
		||||
@ -3,9 +3,9 @@ import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { catchError, map, switchMap } from 'rxjs/operators';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
 | 
			
		||||
import { LightningApiService } from '../lightning-api.service';
 | 
			
		||||
import { isMobile } from '../../shared/common.utils';
 | 
			
		||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-node',
 | 
			
		||||
@ -58,7 +58,6 @@ export class NodeComponent implements OnInit {
 | 
			
		||||
            } else if (socket.indexOf('onion') > -1) {
 | 
			
		||||
              label = 'Tor';
 | 
			
		||||
            }
 | 
			
		||||
            node.flag = getFlagEmoji(node.iso_code);
 | 
			
		||||
            socketsObject.push({
 | 
			
		||||
              label: label,
 | 
			
		||||
              socket: node.public_key + '@' + socket,
 | 
			
		||||
@ -66,6 +65,19 @@ export class NodeComponent implements OnInit {
 | 
			
		||||
          }
 | 
			
		||||
          node.socketsObject = socketsObject;
 | 
			
		||||
          node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count);
 | 
			
		||||
 | 
			
		||||
          if (!node?.country && !node?.city &&
 | 
			
		||||
            !node?.subdivision && !node?.iso) {
 | 
			
		||||
              node.geolocation = null;
 | 
			
		||||
          } else {
 | 
			
		||||
            node.geolocation = <GeolocationData>{
 | 
			
		||||
              country: node.country?.en,
 | 
			
		||||
              city: node.city?.en,
 | 
			
		||||
              subdivision: node.subdivision?.en,
 | 
			
		||||
              iso: node.iso_code,
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return node;
 | 
			
		||||
        }),
 | 
			
		||||
        catchError(err => {
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,16 @@
 | 
			
		||||
<div [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="style === 'graph'" class="card-header">
 | 
			
		||||
    <div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
 | 
			
		||||
      <span i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</span>
 | 
			
		||||
<div *ngIf="channelsObservable | async">
 | 
			
		||||
  <div *ngIf="chartOptions" [class]="'full-container ' + style + (fitContainer ? ' fit-container' : '')">
 | 
			
		||||
    <div *ngIf="style === 'graph'" class="card-header">
 | 
			
		||||
      <div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
 | 
			
		||||
        <span i18n="lightning.nodes-channels-world-map">Lightning nodes channels world map</span>
 | 
			
		||||
      </div>
 | 
			
		||||
      <small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions" (chartInit)="onChartInit($event)"
 | 
			
		||||
      (chartFinished)="onChartFinished($event)">
 | 
			
		||||
    </div>
 | 
			
		||||
    <small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
 | 
			
		||||
    (chartInit)="onChartInit($event)" (chartFinished)="onChartFinished($event)">
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="!chartOptions && style === 'nodepage'" style="padding-top: 30px"></div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ChangeDetectionStrategy, Component, HostListener, Input, Output, EventEmitter, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
 | 
			
		||||
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter, NgZone, OnInit } from '@angular/core';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { Observable, switchMap, tap, zip } from 'rxjs';
 | 
			
		||||
@ -16,14 +16,14 @@ import { isMobile } from 'src/app/shared/common.utils';
 | 
			
		||||
  styleUrls: ['./nodes-channels-map.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class NodesChannelsMap implements OnInit, OnDestroy {
 | 
			
		||||
export class NodesChannelsMap implements OnInit {
 | 
			
		||||
  @Input() style: 'graph' | 'nodepage' | 'widget' | 'channelpage' = 'graph';
 | 
			
		||||
  @Input() publicKey: string | undefined;
 | 
			
		||||
  @Input() channel: any[] = [];
 | 
			
		||||
  @Input() fitContainer = false;
 | 
			
		||||
  @Output() readyEvent = new EventEmitter();
 | 
			
		||||
 | 
			
		||||
  observable$: Observable<any>;
 | 
			
		||||
  channelsObservable: Observable<any>; 
 | 
			
		||||
 | 
			
		||||
  center: number[] | undefined;
 | 
			
		||||
  zoom: number | undefined;
 | 
			
		||||
@ -31,6 +31,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
 | 
			
		||||
  channelOpacity = 0.1;
 | 
			
		||||
  channelColor = '#466d9d';
 | 
			
		||||
  channelCurve = 0;
 | 
			
		||||
  nodeSize = 4;
 | 
			
		||||
 | 
			
		||||
  chartInstance = undefined;
 | 
			
		||||
  chartOptions: EChartsOption = {};
 | 
			
		||||
@ -49,8 +50,6 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
 | 
			
		||||
  ) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy(): void {}
 | 
			
		||||
 | 
			
		||||
  ngOnInit(): void {
 | 
			
		||||
    this.center = this.style === 'widget' ? [0, 40] : [0, 5];
 | 
			
		||||
    this.zoom = 1.3;
 | 
			
		||||
@ -65,8 +64,12 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
 | 
			
		||||
    if (this.style === 'graph') {
 | 
			
		||||
      this.seoService.setTitle($localize`Lightning nodes channels world map`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (['nodepage', 'channelpage'].includes(this.style)) {
 | 
			
		||||
      this.nodeSize = 8;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    this.observable$ = this.activatedRoute.paramMap
 | 
			
		||||
    this.channelsObservable = this.activatedRoute.paramMap
 | 
			
		||||
     .pipe(
 | 
			
		||||
       switchMap((params: ParamMap) => {
 | 
			
		||||
        return zip(
 | 
			
		||||
@ -170,15 +173,8 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
 | 
			
		||||
  prepareChartOptions(nodes, channels) {
 | 
			
		||||
    let title: object;
 | 
			
		||||
    if (channels.length === 0) {
 | 
			
		||||
      title = {
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: 'grey',
 | 
			
		||||
          fontSize: 15
 | 
			
		||||
        },
 | 
			
		||||
        text: $localize`No geolocation data available`,
 | 
			
		||||
        left: 'center',
 | 
			
		||||
        top: 'center'
 | 
			
		||||
      };
 | 
			
		||||
      this.chartOptions = null;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.chartOptions = {
 | 
			
		||||
@ -214,7 +210,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy {
 | 
			
		||||
          data: nodes,
 | 
			
		||||
          coordinateSystem: 'geo',
 | 
			
		||||
          geoIndex: 0,
 | 
			
		||||
          symbolSize: 4,
 | 
			
		||||
          symbolSize: this.nodeSize,
 | 
			
		||||
          tooltip: {
 | 
			
		||||
            show: true,
 | 
			
		||||
            backgroundColor: 'rgba(17, 19, 31, 1)',
 | 
			
		||||
 | 
			
		||||
@ -10,6 +10,7 @@ import { download } from 'src/app/shared/graphs.utils';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { LightningApiService } from '../lightning-api.service';
 | 
			
		||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
 | 
			
		||||
import { isMobile } from 'src/app/shared/common.utils';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-nodes-networks-chart',
 | 
			
		||||
@ -108,19 +109,19 @@ export class NodesNetworksChartComponent implements OnInit {
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(data, maxYAxis) {
 | 
			
		||||
  prepareChartOptions(data, maxYAxis): void {
 | 
			
		||||
    let title: object;
 | 
			
		||||
    if (data.tor_nodes.length === 0) {
 | 
			
		||||
    if (!this.widget && data.tor_nodes.length === 0) {
 | 
			
		||||
      title = {
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: 'grey',
 | 
			
		||||
          fontSize: 15
 | 
			
		||||
        },
 | 
			
		||||
        text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`,
 | 
			
		||||
        text: $localize`Indexing in progess`,
 | 
			
		||||
        left: 'center',
 | 
			
		||||
        top: 'top',
 | 
			
		||||
        top: 'center',
 | 
			
		||||
      };
 | 
			
		||||
    } else if (this.widget) {
 | 
			
		||||
    } else if (data.tor_nodes.length > 0) {
 | 
			
		||||
      title = {
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: 'grey',
 | 
			
		||||
@ -140,11 +141,11 @@ export class NodesNetworksChartComponent implements OnInit {
 | 
			
		||||
        height: this.widget ? 100 : undefined,
 | 
			
		||||
        top: this.widget ? 10 : 40,
 | 
			
		||||
        bottom: this.widget ? 0 : 70,
 | 
			
		||||
        right: (this.isMobile() && this.widget) ? 35 : this.right,
 | 
			
		||||
        left: (this.isMobile() && this.widget) ? 40 :this.left,
 | 
			
		||||
        right: (isMobile() && this.widget) ? 35 : this.right,
 | 
			
		||||
        left: (isMobile() && this.widget) ? 40 :this.left,
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        show: !this.isMobile() || !this.widget,
 | 
			
		||||
        show: !isMobile() || !this.widget,
 | 
			
		||||
        trigger: 'axis',
 | 
			
		||||
        axisPointer: {
 | 
			
		||||
          type: 'line'
 | 
			
		||||
@ -157,7 +158,7 @@ export class NodesNetworksChartComponent implements OnInit {
 | 
			
		||||
          align: 'left',
 | 
			
		||||
        },
 | 
			
		||||
        borderColor: '#000',
 | 
			
		||||
        formatter: (ticks) => {
 | 
			
		||||
        formatter: (ticks): string => {
 | 
			
		||||
          let total = 0;
 | 
			
		||||
          const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
 | 
			
		||||
          let tooltip = `<b style="color: white; margin-left: 2px">${date}</b><br>`;
 | 
			
		||||
@ -180,7 +181,7 @@ export class NodesNetworksChartComponent implements OnInit {
 | 
			
		||||
      },
 | 
			
		||||
      xAxis: data.tor_nodes.length === 0 ? undefined : {
 | 
			
		||||
        type: 'time',
 | 
			
		||||
        splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
 | 
			
		||||
        splitNumber: (isMobile() || this.widget) ? 5 : 10,
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          hideOverlap: true,
 | 
			
		||||
        }
 | 
			
		||||
@ -372,7 +373,7 @@ export class NodesNetworksChartComponent implements OnInit {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onChartInit(ec) {
 | 
			
		||||
  onChartInit(ec): void {
 | 
			
		||||
    if (this.chartInstance !== undefined) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -384,11 +385,7 @@ export class NodesNetworksChartComponent implements OnInit {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isMobile() {
 | 
			
		||||
    return (window.innerWidth <= 767.98);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onSaveChart() {
 | 
			
		||||
  onSaveChart(): void {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    const prevBottom = this.chartOptions.grid.bottom;
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
import { download } from 'src/app/shared/graphs.utils';
 | 
			
		||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
 | 
			
		||||
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
 | 
			
		||||
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
 | 
			
		||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-nodes-per-country-chart',
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,7 @@
 | 
			
		||||
            {{ node.channels }}
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="city text-right text-truncate">
 | 
			
		||||
            {{ node?.city?.en ?? '-' }}
 | 
			
		||||
            <app-geolocation [data]="node.geolocation" [type]="'list-country'"></app-geolocation>
 | 
			
		||||
          </td>
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,8 @@ import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { map, Observable } from 'rxjs';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { getFlagEmoji } from 'src/app/shared/graphs.utils';
 | 
			
		||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
 | 
			
		||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-nodes-per-country',
 | 
			
		||||
@ -29,6 +30,16 @@ export class NodesPerCountry implements OnInit {
 | 
			
		||||
            name: response.country.en,
 | 
			
		||||
            flag: getFlagEmoji(this.route.snapshot.params.country)
 | 
			
		||||
          };
 | 
			
		||||
 | 
			
		||||
          for (const i in response.nodes) {
 | 
			
		||||
            response.nodes[i].geolocation = <GeolocationData>{
 | 
			
		||||
              country: response.nodes[i].country?.en,
 | 
			
		||||
              city: response.nodes[i].city?.en,
 | 
			
		||||
              subdivision: response.nodes[i].subdivision?.en,
 | 
			
		||||
              iso: response.nodes[i].iso_code,
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`);
 | 
			
		||||
          return response.nodes;
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
@ -154,7 +154,7 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
			
		||||
          },
 | 
			
		||||
          borderColor: '#000',
 | 
			
		||||
          formatter: () => {
 | 
			
		||||
            return `<b style="color: white">${isp[1]} (${isp[6]}%)</b><br>` +
 | 
			
		||||
            return `<b style="color: white">${isp[1]} (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)</b><br>` +
 | 
			
		||||
              $localize`${isp[4].toString()} nodes<br>` +
 | 
			
		||||
              $localize`${this.amountShortenerPipe.transform(isp[2] / 100000000, 2)} BTC`
 | 
			
		||||
            ;
 | 
			
		||||
 | 
			
		||||
@ -33,7 +33,7 @@
 | 
			
		||||
            {{ node.channels }}
 | 
			
		||||
          </td>
 | 
			
		||||
          <td class="city text-right text-truncate">
 | 
			
		||||
            {{ node?.city?.en ?? '-' }}
 | 
			
		||||
            <app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
 | 
			
		||||
          </td>
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import { ActivatedRoute } from '@angular/router';
 | 
			
		||||
import { map, Observable } from 'rxjs';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-nodes-per-isp',
 | 
			
		||||
@ -29,6 +30,16 @@ export class NodesPerISP implements OnInit {
 | 
			
		||||
            id: this.route.snapshot.params.isp
 | 
			
		||||
          };
 | 
			
		||||
          this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
 | 
			
		||||
 | 
			
		||||
          for (const i in response.nodes) {
 | 
			
		||||
            response.nodes[i].geolocation = <GeolocationData>{
 | 
			
		||||
              country: response.nodes[i].country?.en,
 | 
			
		||||
              city: response.nodes[i].city?.en,
 | 
			
		||||
              subdivision: response.nodes[i].subdivision?.en,
 | 
			
		||||
              iso: response.nodes[i].iso_code,
 | 
			
		||||
            };
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          return response.nodes;
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
@ -48,4 +48,10 @@
 | 
			
		||||
    <div class="spinner-border text-light"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <div *ngIf="widget && (capacityObservable$ | async) as stats">
 | 
			
		||||
    <div *ngIf="stats.days === 0" class="indexing-message d-flex" i18n="lightning.indexing-in-progress">
 | 
			
		||||
      Indexing in progress
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
@ -131,4 +131,13 @@
 | 
			
		||||
  display: block;
 | 
			
		||||
  max-width: 80px;
 | 
			
		||||
  margin: 15px auto 3px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.indexing-message {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  font-size: 15px;
 | 
			
		||||
  color: grey;
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
  margin-left: calc(50% - 85px);
 | 
			
		||||
  margin-top: -10px;
 | 
			
		||||
}
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core';
 | 
			
		||||
import { EChartsOption, graphic } from 'echarts';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { map, startWith, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { map, share, startWith, switchMap, tap } from 'rxjs/operators';
 | 
			
		||||
import { SeoService } from 'src/app/services/seo.service';
 | 
			
		||||
import { formatNumber } from '@angular/common';
 | 
			
		||||
import { FormBuilder, FormGroup } from '@angular/forms';
 | 
			
		||||
@ -10,6 +10,7 @@ import { MiningService } from 'src/app/services/mining.service';
 | 
			
		||||
import { download } from 'src/app/shared/graphs.utils';
 | 
			
		||||
import { LightningApiService } from '../lightning-api.service';
 | 
			
		||||
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
 | 
			
		||||
import { isMobile } from 'src/app/shared/common.utils';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-lightning-statistics-chart',
 | 
			
		||||
@ -96,12 +97,13 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
              }),
 | 
			
		||||
            );
 | 
			
		||||
        }),
 | 
			
		||||
      )
 | 
			
		||||
        share(),
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  prepareChartOptions(data) {
 | 
			
		||||
  prepareChartOptions(data): void {
 | 
			
		||||
    let title: object;
 | 
			
		||||
    if (data.channel_count.length === 0) {
 | 
			
		||||
    if (!this.widget && data.channel_count.length === 0) {
 | 
			
		||||
      title = {
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: 'grey',
 | 
			
		||||
@ -111,7 +113,7 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
        left: 'center',
 | 
			
		||||
        top: 'center'
 | 
			
		||||
      };
 | 
			
		||||
    } else if (this.widget) {
 | 
			
		||||
    } else if (data.channel_count.length > 0) {
 | 
			
		||||
      title = {
 | 
			
		||||
        textStyle: {
 | 
			
		||||
          color: 'grey',
 | 
			
		||||
@ -138,11 +140,11 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
        height: this.widget ? 100 : undefined,
 | 
			
		||||
        top: this.widget ? 10 : 40,
 | 
			
		||||
        bottom: this.widget ? 0 : 70,
 | 
			
		||||
        right: (this.isMobile() && this.widget) ? 35 : this.right,
 | 
			
		||||
        left: (this.isMobile() && this.widget) ? 40 :this.left,
 | 
			
		||||
        right: (isMobile() && this.widget) ? 35 : this.right,
 | 
			
		||||
        left: (isMobile() && this.widget) ? 40 :this.left,
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        show: !this.isMobile(),
 | 
			
		||||
        show: !isMobile(),
 | 
			
		||||
        trigger: 'axis',
 | 
			
		||||
        axisPointer: {
 | 
			
		||||
          type: 'line'
 | 
			
		||||
@ -155,7 +157,7 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
          align: 'left',
 | 
			
		||||
        },
 | 
			
		||||
        borderColor: '#000',
 | 
			
		||||
        formatter: (ticks) => {
 | 
			
		||||
        formatter: (ticks): string => {
 | 
			
		||||
          let sizeString = '';
 | 
			
		||||
          let weightString = '';
 | 
			
		||||
 | 
			
		||||
@ -169,16 +171,18 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
          const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' });
 | 
			
		||||
 | 
			
		||||
          let tooltip = `<b style="color: white; margin-left: 18px">${date}</b><br>
 | 
			
		||||
          const tooltip = `
 | 
			
		||||
            <b style="color: white; margin-left: 18px">${date}</b><br>
 | 
			
		||||
            <span>${sizeString}</span><br>
 | 
			
		||||
            <span>${weightString}</span>`;
 | 
			
		||||
            <span>${weightString}</span>
 | 
			
		||||
          `;
 | 
			
		||||
 | 
			
		||||
          return tooltip;
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      xAxis: data.channel_count.length === 0 ? undefined : {
 | 
			
		||||
        type: 'time',
 | 
			
		||||
        splitNumber: (this.isMobile() || this.widget) ? 5 : 10,
 | 
			
		||||
        splitNumber: (isMobile() || this.widget) ? 5 : 10,
 | 
			
		||||
        axisLabel: {
 | 
			
		||||
          hideOverlap: true,
 | 
			
		||||
        }
 | 
			
		||||
@ -315,7 +319,7 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onChartInit(ec) {
 | 
			
		||||
  onChartInit(ec): void {
 | 
			
		||||
    if (this.chartInstance !== undefined) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -327,11 +331,7 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isMobile() {
 | 
			
		||||
    return (window.innerWidth <= 767.98);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onSaveChart() {
 | 
			
		||||
  onSaveChart(): void {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    const prevBottom = this.chartOptions.grid.bottom;
 | 
			
		||||
    const now = new Date();
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,120 @@
 | 
			
		||||
export function isMobile() {
 | 
			
		||||
export function isMobile(): boolean {
 | 
			
		||||
  return (window.innerWidth <= 767.98);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getFlagEmoji(countryCode): string {
 | 
			
		||||
  if (!countryCode) {
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
  const codePoints = countryCode
 | 
			
		||||
    .toUpperCase()
 | 
			
		||||
    .split('')
 | 
			
		||||
    .map(char => 127397 + char.charCodeAt());
 | 
			
		||||
  return String.fromCodePoint(...codePoints);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://gist.github.com/calebgrove/c285a9510948b633aa47
 | 
			
		||||
export function convertRegion(input, to: 'name' | 'abbreviated'): string {
 | 
			
		||||
  if (!input) {
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const states = [
 | 
			
		||||
    ['Alabama', 'AL'],
 | 
			
		||||
    ['Alaska', 'AK'],
 | 
			
		||||
    ['American Samoa', 'AS'],
 | 
			
		||||
    ['Arizona', 'AZ'],
 | 
			
		||||
    ['Arkansas', 'AR'],
 | 
			
		||||
    ['Armed Forces Americas', 'AA'],
 | 
			
		||||
    ['Armed Forces Europe', 'AE'],
 | 
			
		||||
    ['Armed Forces Pacific', 'AP'],
 | 
			
		||||
    ['California', 'CA'],
 | 
			
		||||
    ['Colorado', 'CO'],
 | 
			
		||||
    ['Connecticut', 'CT'],
 | 
			
		||||
    ['Delaware', 'DE'],
 | 
			
		||||
    ['District Of Columbia', 'DC'],
 | 
			
		||||
    ['Florida', 'FL'],
 | 
			
		||||
    ['Georgia', 'GA'],
 | 
			
		||||
    ['Guam', 'GU'],
 | 
			
		||||
    ['Hawaii', 'HI'],
 | 
			
		||||
    ['Idaho', 'ID'],
 | 
			
		||||
    ['Illinois', 'IL'],
 | 
			
		||||
    ['Indiana', 'IN'],
 | 
			
		||||
    ['Iowa', 'IA'],
 | 
			
		||||
    ['Kansas', 'KS'],
 | 
			
		||||
    ['Kentucky', 'KY'],
 | 
			
		||||
    ['Louisiana', 'LA'],
 | 
			
		||||
    ['Maine', 'ME'],
 | 
			
		||||
    ['Marshall Islands', 'MH'],
 | 
			
		||||
    ['Maryland', 'MD'],
 | 
			
		||||
    ['Massachusetts', 'MA'],
 | 
			
		||||
    ['Michigan', 'MI'],
 | 
			
		||||
    ['Minnesota', 'MN'],
 | 
			
		||||
    ['Mississippi', 'MS'],
 | 
			
		||||
    ['Missouri', 'MO'],
 | 
			
		||||
    ['Montana', 'MT'],
 | 
			
		||||
    ['Nebraska', 'NE'],
 | 
			
		||||
    ['Nevada', 'NV'],
 | 
			
		||||
    ['New Hampshire', 'NH'],
 | 
			
		||||
    ['New Jersey', 'NJ'],
 | 
			
		||||
    ['New Mexico', 'NM'],
 | 
			
		||||
    ['New York', 'NY'],
 | 
			
		||||
    ['North Carolina', 'NC'],
 | 
			
		||||
    ['North Dakota', 'ND'],
 | 
			
		||||
    ['Northern Mariana Islands', 'NP'],
 | 
			
		||||
    ['Ohio', 'OH'],
 | 
			
		||||
    ['Oklahoma', 'OK'],
 | 
			
		||||
    ['Oregon', 'OR'],
 | 
			
		||||
    ['Pennsylvania', 'PA'],
 | 
			
		||||
    ['Puerto Rico', 'PR'],
 | 
			
		||||
    ['Rhode Island', 'RI'],
 | 
			
		||||
    ['South Carolina', 'SC'],
 | 
			
		||||
    ['South Dakota', 'SD'],
 | 
			
		||||
    ['Tennessee', 'TN'],
 | 
			
		||||
    ['Texas', 'TX'],
 | 
			
		||||
    ['US Virgin Islands', 'VI'],
 | 
			
		||||
    ['Utah', 'UT'],
 | 
			
		||||
    ['Vermont', 'VT'],
 | 
			
		||||
    ['Virginia', 'VA'],
 | 
			
		||||
    ['Washington', 'WA'],
 | 
			
		||||
    ['West Virginia', 'WV'],
 | 
			
		||||
    ['Wisconsin', 'WI'],
 | 
			
		||||
    ['Wyoming', 'WY'],
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  // So happy that Canada and the US have distinct abbreviations
 | 
			
		||||
  const provinces = [
 | 
			
		||||
    ['Alberta', 'AB'],
 | 
			
		||||
    ['British Columbia', 'BC'],
 | 
			
		||||
    ['Manitoba', 'MB'],
 | 
			
		||||
    ['New Brunswick', 'NB'],
 | 
			
		||||
    ['Newfoundland', 'NF'],
 | 
			
		||||
    ['Northwest Territory', 'NT'],
 | 
			
		||||
    ['Nova Scotia', 'NS'],
 | 
			
		||||
    ['Nunavut', 'NU'],
 | 
			
		||||
    ['Ontario', 'ON'],
 | 
			
		||||
    ['Prince Edward Island', 'PE'],
 | 
			
		||||
    ['Quebec', 'QC'],
 | 
			
		||||
    ['Saskatchewan', 'SK'],
 | 
			
		||||
    ['Yukon', 'YT'],
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const regions = states.concat(provinces);
 | 
			
		||||
 | 
			
		||||
  let i; // Reusable loop variable
 | 
			
		||||
  if (to == 'abbreviated') {
 | 
			
		||||
    input = input.replace(/\w\S*/g, function (txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); });
 | 
			
		||||
    for (i = 0; i < regions.length; i++) {
 | 
			
		||||
      if (regions[i][0] == input) {
 | 
			
		||||
        return (regions[i][1]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } else if (to == 'name') {
 | 
			
		||||
    input = input.toUpperCase();
 | 
			
		||||
    for (i = 0; i < regions.length; i++) {
 | 
			
		||||
      if (regions[i][1] == input) {
 | 
			
		||||
        return (regions[i][0]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
<span [innerHTML]="formattedLocation"></span>
 | 
			
		||||
@ -0,0 +1,83 @@
 | 
			
		||||
import { Component, Input, OnChanges } from '@angular/core';
 | 
			
		||||
import { convertRegion, getFlagEmoji } from '../../common.utils';
 | 
			
		||||
 | 
			
		||||
export interface GeolocationData {
 | 
			
		||||
  country: string;
 | 
			
		||||
  city: string;
 | 
			
		||||
  subdivision: string;
 | 
			
		||||
  iso: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-geolocation',
 | 
			
		||||
  templateUrl: './geolocation.component.html',
 | 
			
		||||
  styleUrls: ['./geolocation.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class GeolocationComponent implements OnChanges {
 | 
			
		||||
  @Input() data: GeolocationData;
 | 
			
		||||
  @Input() type: 'node' | 'list-isp' | 'list-country';
 | 
			
		||||
 | 
			
		||||
  formattedLocation: string = '';
 | 
			
		||||
 | 
			
		||||
  ngOnChanges(): void {
 | 
			
		||||
    const city = this.data.city ? this.data.city : '';
 | 
			
		||||
    const subdivisionLikeCity = this.data.city === this.data.subdivision;
 | 
			
		||||
    let subdivision = this.data.subdivision;
 | 
			
		||||
 | 
			
		||||
    if (['US', 'CA'].includes(this.data.iso) === false || (this.type === 'node' && subdivisionLikeCity)) {
 | 
			
		||||
      this.data.subdivision = undefined;
 | 
			
		||||
    } else if (['list-isp', 'list-country'].includes(this.type) === true) {
 | 
			
		||||
      subdivision = convertRegion(this.data.subdivision, 'abbreviated');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.type === 'list-country') {
 | 
			
		||||
      if (this.data.city) {
 | 
			
		||||
        this.formattedLocation += ' ' + city;
 | 
			
		||||
        if (this.data.subdivision) {
 | 
			
		||||
          this.formattedLocation += ', ' + subdivision;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        this.formattedLocation += '-';
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.type === 'list-isp') {
 | 
			
		||||
      this.formattedLocation = getFlagEmoji(this.data.iso);
 | 
			
		||||
      if (this.data.city) {
 | 
			
		||||
        this.formattedLocation += ' ' + city;
 | 
			
		||||
        if (this.data.subdivision) {
 | 
			
		||||
          this.formattedLocation += ', ' + subdivision;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        this.formattedLocation += ' ' + this.data.country;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if (this.type === 'node') {
 | 
			
		||||
      const city = this.data.city ? this.data.city : '';
 | 
			
		||||
 | 
			
		||||
      // City
 | 
			
		||||
      this.formattedLocation = `${city}`;
 | 
			
		||||
 | 
			
		||||
      // ,Subdivision
 | 
			
		||||
      if (this.formattedLocation.length > 0 && !subdivisionLikeCity) {
 | 
			
		||||
        this.formattedLocation += ', ';
 | 
			
		||||
      }
 | 
			
		||||
      if (!subdivisionLikeCity) {
 | 
			
		||||
        this.formattedLocation += `${subdivision}`;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // <br>[flag] County
 | 
			
		||||
      if (this.data?.country.length ?? 0 > 0) {
 | 
			
		||||
        if ((this.formattedLocation?.length ?? 0 > 0) && !subdivisionLikeCity) {
 | 
			
		||||
          this.formattedLocation += '<br>';
 | 
			
		||||
        } else if (this.data.city) {
 | 
			
		||||
          this.formattedLocation += ', ';
 | 
			
		||||
        }
 | 
			
		||||
        this.formattedLocation += `${this.data.country} ${getFlagEmoji(this.data.iso)}`;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -91,13 +91,3 @@ export function detectWebGL() {
 | 
			
		||||
  return (gl && gl instanceof WebGLRenderingContext);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getFlagEmoji(countryCode) {
 | 
			
		||||
  if (!countryCode) {
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
  const codePoints = countryCode
 | 
			
		||||
    .toUpperCase()
 | 
			
		||||
    .split('')
 | 
			
		||||
    .map(char =>  127397 + char.charCodeAt());
 | 
			
		||||
  return String.fromCodePoint(...codePoints);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -82,6 +82,7 @@ import { SatsComponent } from './components/sats/sats.component';
 | 
			
		||||
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
 | 
			
		||||
import { TimestampComponent } from './components/timestamp/timestamp.component';
 | 
			
		||||
import { ToggleComponent } from './components/toggle/toggle.component';
 | 
			
		||||
import { GeolocationComponent } from '../shared/components/geolocation/geolocation.component';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
@ -158,6 +159,7 @@ import { ToggleComponent } from './components/toggle/toggle.component';
 | 
			
		||||
    SearchResultsComponent,
 | 
			
		||||
    TimestampComponent,
 | 
			
		||||
    ToggleComponent,
 | 
			
		||||
    GeolocationComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonModule,
 | 
			
		||||
@ -261,6 +263,7 @@ import { ToggleComponent } from './components/toggle/toggle.component';
 | 
			
		||||
    SearchResultsComponent,
 | 
			
		||||
    TimestampComponent,
 | 
			
		||||
    ToggleComponent,
 | 
			
		||||
    GeolocationComponent,
 | 
			
		||||
  ]
 | 
			
		||||
})
 | 
			
		||||
export class SharedModule {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user