mirror of
https://github.com/mempool/mempool.git
synced 2025-02-22 14:22:44 +01:00
Create geolocation component to format geolocation data
This commit is contained in:
parent
50d99634f7
commit
e437f2125d
15 changed files with 263 additions and 40 deletions
|
@ -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) {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
})
|
||||
);
|
||||
|
|
|
@ -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…
Add table
Reference in a new issue