splatone 0.0.11 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -7
- package/crawler.js +84 -60
- package/lib/splatone.js +17 -3
- package/package.json +2 -1
- package/plugins/flickr/index.js +8 -9
- package/plugins/flickr/worker.js +102 -35
package/README.md
CHANGED
|
@@ -13,6 +13,13 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
|
|
|
13
13
|
|
|
14
14
|
## Change Log
|
|
15
15
|
|
|
16
|
+
### v0.0.11 → v0.0.12
|
|
17
|
+
|
|
18
|
+
* Bottleneckを導入しクエリ間隔を適正値に調整 (3 queries/ 3 sec.)
|
|
19
|
+
* 時間軸分割並列処理のデフォルト化
|
|
20
|
+
* 地理的分割に加えて大量の結果がある場所は時間軸でもクエリを分解する
|
|
21
|
+
* 無効にするときは```--no-p-flickr-Haste```を付与
|
|
22
|
+
|
|
16
23
|
### v0.0.10 → v0.0.11
|
|
17
24
|
|
|
18
25
|
* 時間軸として使用する日付を選択可能に (```--p-flickr-DateMode```)
|
|
@@ -23,6 +30,7 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
|
|
|
23
30
|
* デフォルト値: ```date_upload,date_taken,owner_name,geo,url_s,tags```
|
|
24
31
|
* これらはコマンドライン引数での指定の有無に関わらず付与されます
|
|
25
32
|
* 自動指定時のHexGridの最小サイズを0.5kmに
|
|
33
|
+
* [Bug Fix] 時間軸並列機能のバグ修正
|
|
26
34
|
|
|
27
35
|
### v0.0.8 → v0.0.9 → v0.0.10
|
|
28
36
|
|
|
@@ -59,15 +67,16 @@ Debug
|
|
|
59
67
|
--debug-verbose デバッグ情報出力 [真偽] [デフォルト: false]
|
|
60
68
|
|
|
61
69
|
For flickr Plugin
|
|
62
|
-
--p-flickr-APIKEY
|
|
63
|
-
--p-flickr-
|
|
64
|
-
|
|
70
|
+
--p-flickr-APIKEY Flickr ServiceのAPI KEY [文字列]
|
|
71
|
+
--p-flickr-Extras カンマ区切り/保持する写真のメタデータ(デフォルト値は
|
|
72
|
+
記載の有無に関わらず保持)
|
|
65
73
|
[文字列] [デフォルト: "date_upload,date_taken,owner_name,geo,url_s,tags"]
|
|
66
|
-
--p-flickr-
|
|
74
|
+
--p-flickr-DateMode 利用時間軸(update=Flickr投稿日時/taken=写真撮影日時)
|
|
67
75
|
[選択してください: "upload", "taken"] [デフォルト: "upload"]
|
|
68
|
-
--p-flickr-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
--p-flickr-Haste 時間軸分割並列処理 [真偽] [デフォルト: true]
|
|
77
|
+
--p-flickr-DateMax クローリング期間(最大) UNIX TIMEもしくはYYYY-MM-DD
|
|
78
|
+
[文字列] [デフォルト: 1762904780]
|
|
79
|
+
--p-flickr-DateMin クローリング期間(最小) UNIX TIMEもしくはYYYY-MM-DD
|
|
71
80
|
[文字列] [デフォルト: 1072882800]
|
|
72
81
|
|
|
73
82
|
Visualization (最低一つの指定が必須です)
|
package/crawler.js
CHANGED
|
@@ -8,12 +8,14 @@ import os from 'node:os';
|
|
|
8
8
|
import { EventEmitter } from 'node:events';
|
|
9
9
|
import path, { resolve, dirname } from 'node:path';
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
|
-
import { existsSync, writeFileSync, constants } from 'node:fs';
|
|
11
|
+
import fs, { existsSync, writeFileSync, constants } from 'node:fs';
|
|
12
12
|
import { access, readdir, readFile } from 'node:fs/promises';
|
|
13
|
+
import { MessageChannel } from 'worker_threads';
|
|
13
14
|
|
|
14
15
|
// -------------------------------
|
|
15
16
|
// Third-party
|
|
16
17
|
// -------------------------------
|
|
18
|
+
import Bottleneck from 'bottleneck';
|
|
17
19
|
import express from 'express';
|
|
18
20
|
import open from 'open';
|
|
19
21
|
import Piscina from 'piscina';
|
|
@@ -37,6 +39,10 @@ const app = express();
|
|
|
37
39
|
const port = 3000;
|
|
38
40
|
const title = 'Splatone - Multi-Layer Composite Heatmap Viewer';
|
|
39
41
|
let pluginsOptions = {};
|
|
42
|
+
const flickrLimiter = new Bottleneck({
|
|
43
|
+
maxConcurrent: 5,
|
|
44
|
+
minTime: 350, // 約3req/sec
|
|
45
|
+
});
|
|
40
46
|
|
|
41
47
|
try {
|
|
42
48
|
|
|
@@ -109,7 +115,11 @@ try {
|
|
|
109
115
|
// コマンド例
|
|
110
116
|
// node crawler.js -p flickr -o '{"flickr":{"API_KEY":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}' -k "商業=shop,souvenir,market,supermarket,pharmacy,store,department|食べ物=food,drink,restaurant,cafe,bar|美術 館=museum,art,exhibition,expo,sculpture,heritage|公園=park,garden,flower,green,pond,playground" --vis-bulky
|
|
111
117
|
// node crawler.js -p flickr -k "水域=canal,channel,waterway,river,stream,watercourse,sea,ocean,gulf,bay,strait,lagoon,offshore|橋梁=bridge,overpass,flyover,aqueduct,trestle|通路=street,road,thoroughfare,roadway,avenue,boulevard,lane,alley,roadway,carriageway,highway,motorway|ランドマーク=church,sanctuary,chapel,cathedral,basilica,minster,abbey,temple,shrine" --vis-bulky
|
|
112
|
-
|
|
118
|
+
|
|
119
|
+
// node crawler.js -p flickr -k "水辺=sea,ocean,beach,river,delta,lake,coast,creek|緑地=forest,woods,turf,lawn,jungle,trees,rainforest,grove,savanna,steppe|砂漠=desert,dune,outback,barren,wasteland" --vis-bulky --filed --p-flickr-Haste --p-flickr-DateMode=taken
|
|
120
|
+
|
|
121
|
+
// node crawler.js -p flickr -k "商業=shop,souvenir,market,supermarket,pharmacy,drugstore,store,department,kiosk,bazaar,bookstore,cinema,showroom|飲食=bakery,food,drink,restaurant,cafe,bar,beer,wine,whiskey|文化施設=museum,gallery,theater,concert,library,monument,exhibition,expo,sculpture,heritage|公園=park,garden,flower,green,pond,playground" --vis-bulky --p-flickr-Hates --p-flickr-DateMode=taken
|
|
122
|
+
|
|
113
123
|
let yargv = await yargs(hideBin(process.argv))
|
|
114
124
|
.strict() // 未定義オプションはエラー
|
|
115
125
|
.usage('使い方: $0 [options]')
|
|
@@ -184,7 +194,6 @@ try {
|
|
|
184
194
|
console.warn("--filedと--choppedが両方指定されています。--filedが優先されます。");
|
|
185
195
|
argv.chopped = false;
|
|
186
196
|
}
|
|
187
|
-
console.log(argv);
|
|
188
197
|
pluginsOptions = buildPluginsOptions(argv, plugins.list())
|
|
189
198
|
pluginsOptions[argv.plugin] = await plugins.call(argv.plugin, 'check', pluginsOptions[argv.plugin]);
|
|
190
199
|
return true;
|
|
@@ -313,7 +322,7 @@ try {
|
|
|
313
322
|
console.warn("invalid sessionId:", req.sessionId);
|
|
314
323
|
return;
|
|
315
324
|
}
|
|
316
|
-
const
|
|
325
|
+
const workerOptions = {
|
|
317
326
|
hexGrid: targets[req.sessionId].hex,
|
|
318
327
|
triangles: targets[req.sessionId].triangles,
|
|
319
328
|
sessionId: req.sessionId,
|
|
@@ -321,7 +330,7 @@ try {
|
|
|
321
330
|
pluginOptions: pluginsOptions[argv.plugin]
|
|
322
331
|
};
|
|
323
332
|
//console.log(optPlugin);
|
|
324
|
-
await plugins.call(argv.plugin, 'crawl',
|
|
333
|
+
await plugins.call(argv.plugin, 'crawl', workerOptions);
|
|
325
334
|
}
|
|
326
335
|
catch (e) {
|
|
327
336
|
console.error(e);
|
|
@@ -551,64 +560,79 @@ try {
|
|
|
551
560
|
return { stats, progress, finish: (finish == 0) };
|
|
552
561
|
};
|
|
553
562
|
|
|
554
|
-
async function
|
|
563
|
+
async function runTask_(taskName, data) {
|
|
564
|
+
const { port1, port2 } = new MessageChannel();
|
|
555
565
|
const filename = resolveWorkerFilename(taskName); // ← file URL (href)
|
|
556
566
|
// named export を呼ぶ場合は { name: "関数名" } を追加
|
|
557
|
-
|
|
567
|
+
port1.on('message', (workerResults) => {
|
|
568
|
+
// ここでログ/WebSocket通知/DB書き込みなど何でもOK
|
|
569
|
+
const rtn = workerResults.results;
|
|
570
|
+
const workerOptions = workerResults.workerOptions;
|
|
571
|
+
const currentSessionId = workerOptions.sessionId;
|
|
572
|
+
processing[currentSessionId]--;
|
|
573
|
+
//console.log(rtn);
|
|
574
|
+
crawlers[currentSessionId][rtn.hexId] ??= {};
|
|
575
|
+
crawlers[currentSessionId][rtn.hexId][rtn.category] ??= { items: featureCollection([]) };
|
|
576
|
+
crawlers[currentSessionId][rtn.hexId][rtn.category].ids ??= new Set();
|
|
577
|
+
const duplicates = ((A, B) => new Set([...A].filter(x => B.has(x))))(rtn.ids, crawlers[currentSessionId][rtn.hexId][rtn.category].ids);
|
|
578
|
+
crawlers[currentSessionId][rtn.hexId][rtn.category].ids = new Set([...crawlers[currentSessionId][rtn.hexId][rtn.category].ids, ...rtn.ids]);
|
|
579
|
+
crawlers[currentSessionId][rtn.hexId][rtn.category].final = rtn.final;
|
|
580
|
+
crawlers[currentSessionId][rtn.hexId][rtn.category].crawled ??= 0;
|
|
581
|
+
crawlers[currentSessionId][rtn.hexId][rtn.category].total = rtn.final ? crawlers[currentSessionId][rtn.hexId][rtn.category].ids.size : rtn.total + crawlers[currentSessionId][rtn.hexId][rtn.category].crawled;
|
|
582
|
+
crawlers[currentSessionId][rtn.hexId][rtn.category].crawled = crawlers[currentSessionId][rtn.hexId][rtn.category].ids.size;
|
|
583
|
+
|
|
584
|
+
if (rtn.photos.features.length >= 250 && rtn.photos.features.length == duplicates.size) {
|
|
585
|
+
console.error("ALL DUPLICATE");
|
|
586
|
+
}
|
|
587
|
+
if (argv.debugVerbose) {
|
|
588
|
+
console.log('INFO:', ` ${rtn.hexId} ${rtn.category} ] dup=${duplicates.size}, out=${rtn.outside}, in=${rtn.photos.features.length} || ${crawlers[currentSessionId][rtn.hexId][rtn.category].crawled} / ${crawlers[currentSessionId][rtn.hexId][rtn.category].total}`);
|
|
589
|
+
}
|
|
590
|
+
const photos = featureCollection(rtn.photos.features.filter((f) => !duplicates.has(f.properties.id)));
|
|
591
|
+
crawlers[currentSessionId][rtn.hexId][rtn.category].items
|
|
592
|
+
= concatFC(crawlers[currentSessionId][rtn.hexId][rtn.category].items, photos);
|
|
593
|
+
|
|
594
|
+
const { stats, progress, finish } = statsItems(crawlers[currentSessionId], targets[currentSessionId]);
|
|
595
|
+
io.to(currentSessionId).emit('progress', { hexId: rtn.hexId, progress });
|
|
596
|
+
if (!rtn.final) {
|
|
597
|
+
// 次回クロール用に更新
|
|
598
|
+
rtn.nextPluginOptions.forEach((nextPluginOptions) => {
|
|
599
|
+
const workerOptions_clone = structuredClone(workerOptions);
|
|
600
|
+
workerOptions_clone.pluginOptions = nextPluginOptions;
|
|
601
|
+
api.emit('splatone:start', workerOptions_clone);
|
|
602
|
+
});
|
|
603
|
+
//} else if (finish) {
|
|
604
|
+
} else if (processing[currentSessionId] == 0) {
|
|
605
|
+
if (argv.debugVerbose) {
|
|
606
|
+
console.table(stats);
|
|
607
|
+
}
|
|
608
|
+
api.emit('splatone:finish', workerOptions);
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
const rtn = await piscina.run({ debugVerbose: argv.debugVerbose, port: port2, ...data }, { filename, transferList: [port2] });
|
|
612
|
+
port1.close();
|
|
613
|
+
return rtn;
|
|
558
614
|
}
|
|
615
|
+
const runTask = flickrLimiter.wrap(runTask_);
|
|
559
616
|
|
|
560
|
-
const nParallel = Math.max(1, Math.min(
|
|
617
|
+
const nParallel = Math.max(1, Math.min(12, os.cpus().length))
|
|
561
618
|
const piscina = new Piscina({
|
|
562
|
-
minThreads:
|
|
619
|
+
minThreads: nParallel,
|
|
563
620
|
maxThreads: nParallel,
|
|
564
|
-
idleTimeout: 10_000
|
|
565
|
-
// 注意:ここで filename は渡さない。run 時に切り替える
|
|
621
|
+
idleTimeout: 10_000
|
|
566
622
|
});
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
processing[
|
|
572
|
-
|
|
573
|
-
crawlers[p.sessionId][rtn.hexId] ??= {};
|
|
574
|
-
crawlers[p.sessionId][rtn.hexId][rtn.category] ??= { items: featureCollection([]) };
|
|
575
|
-
crawlers[p.sessionId][rtn.hexId][rtn.category].ids ??= new Set();
|
|
576
|
-
const duplicates = ((A, B) => new Set([...A].filter(x => B.has(x))))(rtn.ids, crawlers[p.sessionId][rtn.hexId][rtn.category].ids);
|
|
577
|
-
crawlers[p.sessionId][rtn.hexId][rtn.category].ids = new Set([...crawlers[p.sessionId][rtn.hexId][rtn.category].ids, ...rtn.ids]);
|
|
578
|
-
crawlers[p.sessionId][rtn.hexId][rtn.category].final = rtn.final;
|
|
579
|
-
crawlers[p.sessionId][rtn.hexId][rtn.category].crawled ??= 0;
|
|
580
|
-
crawlers[p.sessionId][rtn.hexId][rtn.category].total = rtn.final ? crawlers[p.sessionId][rtn.hexId][rtn.category].ids.size : rtn.total + crawlers[p.sessionId][rtn.hexId][rtn.category].crawled;
|
|
581
|
-
crawlers[p.sessionId][rtn.hexId][rtn.category].crawled = crawlers[p.sessionId][rtn.hexId][rtn.category].ids.size;
|
|
582
|
-
|
|
583
|
-
if (argv.debugVerbose) {
|
|
584
|
-
console.log('INFO:', ` ${rtn.hexId} ${rtn.category} ] dup=${duplicates.size}, out=${rtn.outside}, in=${rtn.photos.features.length} || ${crawlers[p.sessionId][rtn.hexId][rtn.category].crawled} / ${crawlers[p.sessionId][rtn.hexId][rtn.category].total}`);
|
|
585
|
-
}
|
|
586
|
-
const photos = featureCollection(rtn.photos.features.filter((f) => !duplicates.has(f.properties.id)));
|
|
587
|
-
crawlers[p.sessionId][rtn.hexId][rtn.category].items
|
|
588
|
-
= concatFC(crawlers[p.sessionId][rtn.hexId][rtn.category].items, photos);
|
|
589
|
-
|
|
590
|
-
const { stats, progress, finish } = statsItems(crawlers[p.sessionId], targets[p.sessionId]);
|
|
591
|
-
io.to(p.sessionId).emit('progress', { hexId: rtn.hexId, progress });
|
|
592
|
-
if (!rtn.final) {
|
|
593
|
-
// 次回クロール用に更新
|
|
594
|
-
rtn.nextPluginOptions.forEach((nextPluginOptions) => {
|
|
595
|
-
const p_clone = structuredClone(p);
|
|
596
|
-
p_clone.pluginOptions = nextPluginOptions
|
|
597
|
-
api.emit('splatone:start', p_clone);
|
|
598
|
-
});
|
|
599
|
-
//} else if (finish) {
|
|
600
|
-
} else if (processing[p.sessionId] == 0) {
|
|
601
|
-
if (argv.debugVerbose) {
|
|
602
|
-
console.table(stats);
|
|
603
|
-
}
|
|
604
|
-
api.emit('splatone:finish', p);
|
|
605
|
-
}
|
|
623
|
+
|
|
624
|
+
await subscribe('splatone:start', async workerOptions => {
|
|
625
|
+
//console.log('[splatone:start]', workerOptions);
|
|
626
|
+
const currentSessionId = workerOptions.sessionId;
|
|
627
|
+
processing[currentSessionId] = (processing[currentSessionId] ?? 0) + 1;
|
|
628
|
+
runTask(workerOptions.plugin, workerOptions);
|
|
606
629
|
});
|
|
607
630
|
|
|
608
|
-
await subscribe('splatone:finish', async
|
|
631
|
+
await subscribe('splatone:finish', async workerOptions => {
|
|
632
|
+
const currentSessionId = workerOptions.sessionId;
|
|
609
633
|
const resultId = uniqid();
|
|
610
|
-
const result = crawlers[
|
|
611
|
-
const target = targets[
|
|
634
|
+
const result = crawlers[currentSessionId];
|
|
635
|
+
const target = targets[currentSessionId];
|
|
612
636
|
let geoJson = Object.fromEntries(Object.entries(visualizers).map(([vis, v]) => [vis, v.getFutureCollection(result, target)]));
|
|
613
637
|
|
|
614
638
|
//console.log('[splatone:finish]');
|
|
@@ -616,7 +640,7 @@ try {
|
|
|
616
640
|
if (argv.chopped || argv.filed) {
|
|
617
641
|
throw new RangeError("Invalid string length");
|
|
618
642
|
}
|
|
619
|
-
await io.to(
|
|
643
|
+
await io.to(currentSessionId).timeout(120000).emitWithAck('result', {
|
|
620
644
|
resultId,
|
|
621
645
|
geoJson,
|
|
622
646
|
palette: target["splatonePalette"],
|
|
@@ -629,7 +653,7 @@ try {
|
|
|
629
653
|
if (argv.debugVerbose) {
|
|
630
654
|
console.warn("[WARN] " + msg);
|
|
631
655
|
}
|
|
632
|
-
io.to(
|
|
656
|
+
io.to(currentSessionId).timeout(5000).emit('toast', {
|
|
633
657
|
text: msg,
|
|
634
658
|
class: "warning"
|
|
635
659
|
});
|
|
@@ -641,7 +665,7 @@ try {
|
|
|
641
665
|
if (path.length !== 0) {
|
|
642
666
|
if (kind === "primitive" || kind === "null") {
|
|
643
667
|
//console.log(path.join("."), "=>", `(${kind}:${type})`, value);
|
|
644
|
-
const ackrtn = await io.to(
|
|
668
|
+
const ackrtn = await io.to(currentSessionId).timeout(120000).emitWithAck('result-chunk', {
|
|
645
669
|
resultId,
|
|
646
670
|
path,
|
|
647
671
|
kind,
|
|
@@ -654,7 +678,7 @@ try {
|
|
|
654
678
|
if (path.at(-2) == "features" && Number.isInteger(path.at(-1))) {
|
|
655
679
|
current_features++;
|
|
656
680
|
}
|
|
657
|
-
const ackrtn = await io.to(
|
|
681
|
+
const ackrtn = await io.to(currentSessionId).timeout(120000).emitWithAck('result-chunk', {
|
|
658
682
|
resultId,
|
|
659
683
|
path,
|
|
660
684
|
kind,
|
|
@@ -664,7 +688,7 @@ try {
|
|
|
664
688
|
//console.log("\tACK", ackrtn);
|
|
665
689
|
} else if (kind === "array") {
|
|
666
690
|
//console.log(path.join("."), "=>", `(${kind}:${type})`);
|
|
667
|
-
const ackrtn = await io.to(
|
|
691
|
+
const ackrtn = await io.to(currentSessionId).timeout(120000).emitWithAck('result-chunk', {
|
|
668
692
|
resultId,
|
|
669
693
|
path,
|
|
670
694
|
kind,
|
|
@@ -682,7 +706,7 @@ try {
|
|
|
682
706
|
try {
|
|
683
707
|
const outPath = await saveGeoJsonObjectAsStream(geoJson, 'result.' + resultId + '.json');
|
|
684
708
|
console.log('saved:', outPath);
|
|
685
|
-
const ackrtn = await io.to(
|
|
709
|
+
const ackrtn = await io.to(currentSessionId).timeout(120000).emitWithAck('result-file', {
|
|
686
710
|
resultId,
|
|
687
711
|
});
|
|
688
712
|
} catch (err) {
|
|
@@ -690,7 +714,7 @@ try {
|
|
|
690
714
|
process.exitCode = 1;
|
|
691
715
|
}
|
|
692
716
|
}
|
|
693
|
-
await io.to(
|
|
717
|
+
await io.to(currentSessionId).timeout(120000).emitWithAck('result', {
|
|
694
718
|
resultId,
|
|
695
719
|
geoJson: null, /*geoJsonは送らない*/
|
|
696
720
|
palette: target["splatonePalette"],
|
package/lib/splatone.js
CHANGED
|
@@ -4,8 +4,8 @@ import { point, distance, rhumbDistance, bbox as turfBbox } from '@turf/turf';
|
|
|
4
4
|
import { createWriteStream } from 'node:fs';
|
|
5
5
|
import { mkdir, constants, access, readFile } from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { pipeline
|
|
8
|
-
import { promisify } from 'node:util';
|
|
7
|
+
import { pipeline } from 'node:stream/promises';
|
|
8
|
+
//import { promisify } from 'node:util';
|
|
9
9
|
import { JsonStreamStringify } from 'json-stream-stringify';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -45,6 +45,21 @@ export function bboxSizeOf(geojson, units = "kilometers", method = "geodesic") {
|
|
|
45
45
|
return bboxSize(turfBbox(geojson), units, method);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
*
|
|
50
|
+
* @param {*} dateTimeStr 日付文字列
|
|
51
|
+
* @returns UNIX Time Stamp (sec.)
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
export function toUnixSeconds(dateTimeStr) {
|
|
55
|
+
const [datePart, timePart] = dateTimeStr.split(' ');
|
|
56
|
+
const [year, month, day] = datePart.split('-').map(Number);
|
|
57
|
+
const [hour, minute, second] = timePart.split(':').map(Number);
|
|
58
|
+
|
|
59
|
+
// JSの month は 0 始まり
|
|
60
|
+
const d = new Date(year, month - 1, day, hour, minute, second);
|
|
61
|
+
return Math.floor(d.getTime() / 1000);
|
|
62
|
+
}
|
|
48
63
|
|
|
49
64
|
// visit({ key, value, path, parent, depth, kind, type, isLeaf, isNull })
|
|
50
65
|
// kind: 'array' | 'object' | 'primitive' | 'null'
|
|
@@ -133,7 +148,6 @@ function describeType(value) {
|
|
|
133
148
|
const ctor = value?.constructor?.name ?? "Object";
|
|
134
149
|
return { kind: "object", type: ctor, isLeaf: false, isNull: false };
|
|
135
150
|
}
|
|
136
|
-
const pipeline = promisify(pipelineCb);
|
|
137
151
|
export async function saveGeoJsonObjectAsStream(geoJsonObject, outfile) {
|
|
138
152
|
const dir = path.join('public', 'out');
|
|
139
153
|
await mkdir(dir, { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "splatone",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Multi-layer Composite Heatmap",
|
|
5
5
|
"homepage": "https://github.com/YokoyamaLab/Splatone#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@turf/turf": "^7.2.0",
|
|
28
|
+
"bottleneck": "^2.19.5",
|
|
28
29
|
"chroma-js": "^3.1.2",
|
|
29
30
|
"ejs": "^3.1.10",
|
|
30
31
|
"express": "^5.1.0",
|
package/plugins/flickr/index.js
CHANGED
|
@@ -15,7 +15,7 @@ export default class FlickrPlugin extends PluginBase {
|
|
|
15
15
|
}
|
|
16
16
|
async yargv(yargv) {
|
|
17
17
|
// 必須項目にすると、このプラグインを使用しない時も必須になります。
|
|
18
|
-
//
|
|
18
|
+
// 必須項目は作らず、もしプラグインを使う上での制約違反はinitで例外を投げてください。
|
|
19
19
|
return yargv.option(this.argKey('APIKEY'), {
|
|
20
20
|
group: 'For ' + this.id + ' Plugin',
|
|
21
21
|
type: 'string',
|
|
@@ -45,9 +45,9 @@ export default class FlickrPlugin extends PluginBase {
|
|
|
45
45
|
description: '利用時間軸(update=Flickr投稿日時/taken=写真撮影日時)'
|
|
46
46
|
}).option(this.argKey('Haste'), {
|
|
47
47
|
group: 'For ' + this.id + ' Plugin',
|
|
48
|
-
default:
|
|
48
|
+
default: true,
|
|
49
49
|
type: 'boolean',
|
|
50
|
-
description: '時間軸分割並列処理
|
|
50
|
+
description: '時間軸分割並列処理'
|
|
51
51
|
}).option(this.argKey('DateMax'), {
|
|
52
52
|
group: 'For ' + this.id + ' Plugin',
|
|
53
53
|
type: 'string',
|
|
@@ -133,17 +133,16 @@ export default class FlickrPlugin extends PluginBase {
|
|
|
133
133
|
}
|
|
134
134
|
const hexQuery = {};
|
|
135
135
|
const ks = Object.keys(hexGrid.features);
|
|
136
|
-
|
|
136
|
+
ks.map(k => {
|
|
137
137
|
const item = hexGrid.features[k];
|
|
138
138
|
hexQuery[item.properties.hexId] = {};
|
|
139
139
|
const cks = Object.keys(categories);
|
|
140
|
-
|
|
140
|
+
cks.map(ck => {
|
|
141
141
|
const tags = categories[ck];
|
|
142
142
|
//console.log("tag=",ck,"/",tags);
|
|
143
143
|
hexQuery[item.properties.hexId][ck] = { photos: [], tags, final: false };
|
|
144
|
-
this.api.emit('splatone:start', {
|
|
144
|
+
this.api.emit('splatone:start', { //WorkerOptions
|
|
145
145
|
plugin: this.id,
|
|
146
|
-
//API_KEY: this.APIKEY ?? pluginOptions.APIKEY,
|
|
147
146
|
hex: item,
|
|
148
147
|
triangles: getTrianglesInHex(item, triangles),
|
|
149
148
|
bbox: bbox(item.geometry),
|
|
@@ -152,8 +151,8 @@ export default class FlickrPlugin extends PluginBase {
|
|
|
152
151
|
pluginOptions,
|
|
153
152
|
sessionId
|
|
154
153
|
});
|
|
155
|
-
})
|
|
156
|
-
})
|
|
154
|
+
});
|
|
155
|
+
});
|
|
157
156
|
return `${this.id}, ${this.options.API_KEY}, ${hexGrid.features.length} bboxes processed.`;
|
|
158
157
|
}
|
|
159
158
|
}
|
package/plugins/flickr/worker.js
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
import { createFlickr } from "flickr-sdk"
|
|
2
2
|
import { point, featureCollection } from "@turf/helpers";
|
|
3
3
|
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
|
|
4
|
+
import { toUnixSeconds } from '#lib/splatone';
|
|
4
5
|
|
|
5
6
|
export default async function ({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
port,
|
|
8
|
+
debugVerbose,
|
|
9
|
+
plugin,
|
|
10
|
+
hex,
|
|
11
|
+
triangles,
|
|
12
|
+
bbox,
|
|
13
|
+
category,
|
|
14
|
+
tags,
|
|
15
|
+
pluginOptions,
|
|
16
|
+
sessionId
|
|
13
17
|
}) {
|
|
18
|
+
debugVerbose = true;
|
|
14
19
|
//console.log("{PLUGIN}", pluginOptions);
|
|
15
20
|
const { flickr } = createFlickr(pluginOptions["APIKEY"]);
|
|
21
|
+
if (!pluginOptions.TermId) {
|
|
22
|
+
//初期TermId
|
|
23
|
+
pluginOptions.TermId = 'a';
|
|
24
|
+
}
|
|
16
25
|
const baseParams = {
|
|
17
26
|
bbox: bbox.join(','),
|
|
18
27
|
tags: tags,
|
|
19
28
|
extras: pluginOptions["Extras"],
|
|
20
29
|
sort: pluginOptions["DateMode"] == "upload" ? "date-posted-desc" : "date-taken-desc"
|
|
21
30
|
};
|
|
22
|
-
baseParams[pluginOptions["
|
|
23
|
-
baseParams[pluginOptions["
|
|
31
|
+
baseParams[pluginOptions["DateMode"] == "upload" ? 'max_upload_date' : 'max_taken_date'] = pluginOptions["DateMax"];
|
|
32
|
+
baseParams[pluginOptions["DateMode"] == "upload" ? 'min_upload_date' : 'min_taken_date'] = pluginOptions["DateMin"];
|
|
24
33
|
//console.log("[baseParams]",baseParams);
|
|
25
34
|
const res = await flickr("flickr.photos.search", {
|
|
26
35
|
...baseParams,
|
|
@@ -28,7 +37,7 @@ export default async function ({
|
|
|
28
37
|
per_page: 250,
|
|
29
38
|
page: 1,
|
|
30
39
|
});
|
|
31
|
-
//console.log(
|
|
40
|
+
//console.log(res);
|
|
32
41
|
const ids = [];
|
|
33
42
|
const authors = {};
|
|
34
43
|
const photos = featureCollection(res.photos.photo.filter(photo => {
|
|
@@ -57,33 +66,91 @@ export default async function ({
|
|
|
57
66
|
//console.log(JSON.stringify(photos, null, 4));
|
|
58
67
|
|
|
59
68
|
const nextPluginOptionsDelta = [];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const window = res.photos.photo.length == 0 ? 0 : res.photos.photo[0].dateupload - res.photos.photo[res.photos.photo.length - 1].dateupload;
|
|
65
|
-
if (Object.keys(authors).length == 1 && window < 60 * 60) {
|
|
66
|
-
const skip = window < 5 ? 0.1 : 12;
|
|
67
|
-
console.warn("[Warning]", `High posting activity detected for ${Object.keys(authors)} within ${window} s. the crawler will skip the next ${skip} hours.`);
|
|
68
|
-
next_max_date -= 60 * 60 * skip;
|
|
69
|
-
}
|
|
70
|
-
if (pluginOptions["Haste"] && res.photos.pages > 4) {
|
|
71
|
-
//結果の最大・最小を2分割
|
|
72
|
-
const mid = ((next_max_date - pluginOptions.DateMin) / 2) + pluginOptions.DateMin;
|
|
73
|
-
nextPluginOptionsDelta.push({
|
|
74
|
-
'DateMax': next_max_date,
|
|
75
|
-
'DateMin': mid
|
|
76
|
-
});
|
|
77
|
-
nextPluginOptionsDelta.push({
|
|
78
|
-
'DateMax': mid,
|
|
79
|
-
'DateMin': pluginOptions.DateMin
|
|
80
|
-
});
|
|
69
|
+
if (res.photos.photo.length == 0) {
|
|
70
|
+
if (debugVerbose) {
|
|
71
|
+
console.log(`Zero (${hex.properties.hexId} - ${category} - ${pluginOptions.TermId})`);
|
|
72
|
+
}
|
|
81
73
|
} else {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
74
|
+
let minDate, maxDate;
|
|
75
|
+
try {
|
|
76
|
+
minDate = res.photos.photo[res.photos.photo.length - 1].dateupload;
|
|
77
|
+
maxDate = res.photos.photo[0].dateupload
|
|
78
|
+
|
|
79
|
+
if (pluginOptions["DateMode"] == "taken") {
|
|
80
|
+
minDate = toUnixSeconds(res.photos.photo[res.photos.photo.length - 1].datetaken)
|
|
81
|
+
maxDate = toUnixSeconds(res.photos.photo[0].datetaken);
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
console.log("DateMode ERROR")
|
|
85
|
+
console.log(res)
|
|
86
|
+
}
|
|
87
|
+
let next_max_date
|
|
88
|
+
= res.photos.photo.length > 0
|
|
89
|
+
? (minDate) - (minDate == maxDate ? 1 : 0)
|
|
90
|
+
: null;
|
|
91
|
+
const window = res.photos.photo.length == 0 ? 0 : maxDate - minDate;
|
|
92
|
+
if (Object.keys(authors).length == 1 && res.photos.photo.length >= 250 && window < 60 * 60) {
|
|
93
|
+
const skip = window < 0 ? Math.abs(window) * 1.1 : (window < 5 ? 0.1 : 12);
|
|
94
|
+
if (debugVerbose) {
|
|
95
|
+
console.warn("[Warning]", (window < 0 ? "[[[Negative Time Window Error]]]" : ""), `High posting activity detected for ${Object.keys(authors)} within ${window} s. the crawler will skip the next ${skip} hours.`);
|
|
96
|
+
}
|
|
97
|
+
next_max_date -= 60 * 60 * skip;
|
|
98
|
+
}
|
|
99
|
+
if (pluginOptions["Haste"] && res.photos.pages > 4) {
|
|
100
|
+
//結果の最大・最小を2分割
|
|
101
|
+
const mid = Math.round(((next_max_date - pluginOptions.DateMin) / 2) + pluginOptions.DateMin);
|
|
102
|
+
if (debugVerbose) {
|
|
103
|
+
console.log(`Split(${hex.properties.hexId} - ${category} - ${pluginOptions.TermId}):`, pluginOptions.DateMin, mid, next_max_date);
|
|
104
|
+
}
|
|
105
|
+
nextPluginOptionsDelta.push({
|
|
106
|
+
'DateMax': next_max_date,
|
|
107
|
+
'DateMin': mid,
|
|
108
|
+
'TermId': pluginOptions.TermId + 'a'
|
|
109
|
+
});
|
|
110
|
+
nextPluginOptionsDelta.push({
|
|
111
|
+
'DateMax': mid,
|
|
112
|
+
'DateMin': pluginOptions.DateMin,
|
|
113
|
+
'TermId': pluginOptions.TermId + 'b'
|
|
114
|
+
});
|
|
115
|
+
} else if (res.photos.photo.length < res.photos.total) {
|
|
116
|
+
if (debugVerbose) {
|
|
117
|
+
console.log(`Continue[${res.photos.pages} pages](${hex.properties.hexId} - ${category} - ${pluginOptions.TermId}):`, pluginOptions.DateMin, next_max_date);
|
|
118
|
+
}
|
|
119
|
+
nextPluginOptionsDelta.push({
|
|
120
|
+
'DateMax': next_max_date,
|
|
121
|
+
'DateMin': pluginOptions.DateMin,
|
|
122
|
+
});
|
|
123
|
+
} else {
|
|
124
|
+
//final
|
|
125
|
+
if (debugVerbose) {
|
|
126
|
+
console.log(`Final(${hex.properties.hexId} - ${category} - ${pluginOptions.TermId}):`, pluginOptions.DateMin, next_max_date);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
86
129
|
}
|
|
130
|
+
port.postMessage({
|
|
131
|
+
workerOptions: {
|
|
132
|
+
plugin,
|
|
133
|
+
hex,
|
|
134
|
+
triangles,
|
|
135
|
+
bbox,
|
|
136
|
+
category,
|
|
137
|
+
tags,
|
|
138
|
+
pluginOptions,
|
|
139
|
+
sessionId,
|
|
140
|
+
},
|
|
141
|
+
results: {
|
|
142
|
+
photos,
|
|
143
|
+
hexId: hex.properties.hexId,
|
|
144
|
+
tags,
|
|
145
|
+
category,
|
|
146
|
+
nextPluginOptions: nextPluginOptionsDelta.map(e => { return { ...pluginOptions, ...e } }),
|
|
147
|
+
total: res.photos.total,
|
|
148
|
+
outside: outside,
|
|
149
|
+
ids,
|
|
150
|
+
final: nextPluginOptionsDelta.length == 0//res.photos.photo.length == res.photos.total
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
return true;
|
|
87
154
|
return {
|
|
88
155
|
photos,
|
|
89
156
|
hexId: hex.properties.hexId,
|