diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts
index 295e69bc0..e9be9c2e0 100644
--- a/backend/src/api/explorer/nodes.api.ts
+++ b/backend/src/api/explorer/nodes.api.ts
@@ -385,9 +385,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
@@ -395,15 +396,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) {
@@ -417,7 +422,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
@@ -425,8 +431,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
`;
@@ -435,6 +443,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 {