Merge pull request #2324 from mempool/nymkappa/feature/improve-location

Create geolocation component to format geolocation data
This commit is contained in:
wiz 2022-08-21 21:12:04 +09:00 committed by GitHub
commit 7c1e35ae3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 263 additions and 40 deletions

View file

@ -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) {

View file

@ -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';

View file

@ -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>
&nbsp;
<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>

View file

@ -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 => {

View file

@ -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',

View file

@ -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>

View file

@ -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;
})

View file

@ -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>

View file

@ -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;
})
);

View file

@ -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]);
}
}
}
}

View file

@ -0,0 +1 @@
<span [innerHTML]="formattedLocation"></span>

View file

@ -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;
}
}
}

View file

@ -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);
}

View file

@ -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 {