From e437f2125d51a213f3a33ff0fd1e6694c7fedfe3 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 18 Aug 2022 17:14:09 +0200 Subject: [PATCH] Create geolocation component to format geolocation data --- backend/src/api/explorer/nodes.api.ts | 21 +++- .../lightning/node/node-preview.component.ts | 2 +- .../app/lightning/node/node.component.html | 18 +-- .../src/app/lightning/node/node.component.ts | 16 ++- .../nodes-per-country-chart.component.ts | 2 +- .../nodes-per-country.component.html | 2 +- .../nodes-per-country.component.ts | 13 +- .../nodes-per-isp.component.html | 2 +- .../nodes-per-isp/nodes-per-isp.component.ts | 11 ++ frontend/src/app/shared/common.utils.ts | 119 +++++++++++++++++- .../geolocation/geolocation.component.html | 1 + .../geolocation/geolocation.component.scss | 0 .../geolocation/geolocation.component.ts | 83 ++++++++++++ frontend/src/app/shared/graphs.utils.ts | 10 -- frontend/src/app/shared/shared.module.ts | 3 + 15 files changed, 263 insertions(+), 40 deletions(-) create mode 100644 frontend/src/app/shared/components/geolocation/geolocation.component.html create mode 100644 frontend/src/app/shared/components/geolocation/geolocation.component.scss create mode 100644 frontend/src/app/shared/components/geolocation/geolocation.component.ts diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index e59770d50..0a3064d8d 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -284,9 +284,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 @@ -294,15 +295,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) { @@ -316,7 +321,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 @@ -324,8 +330,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 `; @@ -334,6 +342,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) { diff --git a/frontend/src/app/lightning/node/node-preview.component.ts b/frontend/src/app/lightning/node/node-preview.component.ts index 6344a38b2..0d6908eb1 100644 --- a/frontend/src/app/lightning/node/node-preview.component.ts +++ b/frontend/src/app/lightning/node/node-preview.component.ts @@ -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'; diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index de6c816f0..e2e500ac5 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -42,24 +42,10 @@ - + Location - {{ node.city.en }}, {{ node.subdivision.en }} -
- - {{ node.country.en }} -   - {{ node.flag }} - - - - - Location - - - {{ node.country.en }} {{ node.flag }} - + diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index a81849388..8ddaacf95 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -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 = { + country: node.country?.en, + city: node.city?.en, + subdivision: node.subdivision?.en, + iso: node.iso_code, + }; + } + return node; }), catchError(err => { diff --git a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts index c6a640015..09b00e032 100644 --- a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts +++ b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.ts @@ -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', diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html index 2896b4544..16f4265a2 100644 --- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html +++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.html @@ -36,7 +36,7 @@ {{ node.channels }} - {{ node?.city?.en ?? '-' }} + diff --git a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts index a247baadf..644e6741a 100644 --- a/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts +++ b/frontend/src/app/lightning/nodes-per-country/nodes-per-country.component.ts @@ -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 = { + 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; }) diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html index b69e749e6..a8931d843 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.html @@ -33,7 +33,7 @@ {{ node.channels }} - {{ node?.city?.en ?? '-' }} + diff --git a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts index 4c7667f5d..cc57056fc 100644 --- a/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp/nodes-per-isp.component.ts @@ -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 = { + 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; }) ); diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts index 419c1665d..d38583217 100644 --- a/frontend/src/app/shared/common.utils.ts +++ b/frontend/src/app/shared/common.utils.ts @@ -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]); + } + } + } +} diff --git a/frontend/src/app/shared/components/geolocation/geolocation.component.html b/frontend/src/app/shared/components/geolocation/geolocation.component.html new file mode 100644 index 000000000..2788cd4c1 --- /dev/null +++ b/frontend/src/app/shared/components/geolocation/geolocation.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/app/shared/components/geolocation/geolocation.component.scss b/frontend/src/app/shared/components/geolocation/geolocation.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/app/shared/components/geolocation/geolocation.component.ts b/frontend/src/app/shared/components/geolocation/geolocation.component.ts new file mode 100644 index 000000000..d1c02e53a --- /dev/null +++ b/frontend/src/app/shared/components/geolocation/geolocation.component.ts @@ -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}`; + } + + //
[flag] County + if (this.data?.country.length ?? 0 > 0) { + if ((this.formattedLocation?.length ?? 0 > 0) && !subdivisionLikeCity) { + this.formattedLocation += '
'; + } else if (this.data.city) { + this.formattedLocation += ', '; + } + this.formattedLocation += `${this.data.country} ${getFlagEmoji(this.data.iso)}`; + } + + return; + } + } +} diff --git a/frontend/src/app/shared/graphs.utils.ts b/frontend/src/app/shared/graphs.utils.ts index 019ca49e4..90977e6f4 100644 --- a/frontend/src/app/shared/graphs.utils.ts +++ b/frontend/src/app/shared/graphs.utils.ts @@ -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); -} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index bfd47e411..f9de57834 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -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 {