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 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 Flickr ServiceのAPI KEY [文字列]
63
- --p-flickr-extras カンマ区切り/保持する写真のメタデータ(デフォルト値は記
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-Date 利用時間軸(update=Flickr投稿日時/taken=写真撮影日時)
74
+ --p-flickr-DateMode 利用時間軸(update=Flickr投稿日時/taken=写真撮影日時)
67
75
  [選択してください: "upload", "taken"] [デフォルト: "upload"]
68
- --p-flickr-DateMax クローリング期間(最大) UNIX TIMEもしくはYYYY-MM-DD
69
- [文字列] [デフォルト: 1762701683]
70
- --p-flickr-DateMin クローリング期間(最小) UNIX TIMEもしくはYYYY-MM-DD
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
- // 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
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 optPlugin = {
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', optPlugin);
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 runTask(taskName, data) {
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
- return piscina.run(data, { filename });
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(6, os.cpus().length))
617
+ const nParallel = Math.max(1, Math.min(12, os.cpus().length))
561
618
  const piscina = new Piscina({
562
- minThreads: 1,
619
+ minThreads: nParallel,
563
620
  maxThreads: nParallel,
564
- idleTimeout: 10_000,
565
- // 注意:ここで filename は渡さない。run 時に切り替える
621
+ idleTimeout: 10_000
566
622
  });
567
- await subscribe('splatone:start', async p => {
568
- //console.log('[splatone:start]', p);
569
- processing[p.sessionId] = (processing[p.sessionId] ?? 0) + 1;
570
- let rtn = await runTask(p.plugin, p);
571
- processing[p.sessionId]--;
572
- //console.log('[splatone:done]', p.plugin, rtn.photos.features.length,"photos are collected in hex",rtn.hexId,"tags:",rtn.tags,"final:",rtn.final);
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 p => {
631
+ await subscribe('splatone:finish', async workerOptions => {
632
+ const currentSessionId = workerOptions.sessionId;
609
633
  const resultId = uniqid();
610
- const result = crawlers[p.sessionId];
611
- const target = targets[p.sessionId];
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(p.sessionId).timeout(120000).emitWithAck('result', {
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(p.sessionId).timeout(5000).emit('toast', {
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(p.sessionId).timeout(120000).emitWithAck('result-chunk', {
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(p.sessionId).timeout(120000).emitWithAck('result-chunk', {
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(p.sessionId).timeout(120000).emitWithAck('result-chunk', {
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(p.sessionId).timeout(120000).emitWithAck('result-file', {
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(p.sessionId).timeout(120000).emitWithAck('result', {
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 as pipelineCb } from 'node:stream';
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.11",
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",
@@ -15,7 +15,7 @@ export default class FlickrPlugin extends PluginBase {
15
15
  }
16
16
  async yargv(yargv) {
17
17
  // 必須項目にすると、このプラグインを使用しない時も必須になります。
18
- // 必須項目は作らず、initで例外を投げてください。
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: false,
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
- await Promise.all(ks.map(async k => {
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
- await Promise.all(cks.map(ck => {
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
  }
@@ -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
- //API_KEY = "",
7
- bbox = [0, 0, 0, 0],
8
- tags = "",
9
- category = "",
10
- hex = null,
11
- triangles = null,
12
- pluginOptions
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["Date"] == "upload" ? 'max_upload_date' : 'max_taken_date'] = pluginOptions["DateMax"];
23
- baseParams[pluginOptions["Date"] == "upload" ? 'min_upload_date' : 'min_taken_date'] = pluginOptions["DateMin"];
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(baseParams);
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
- let next_max_date
61
- = res.photos.photo.length > 0
62
- ? (res.photos.photo[res.photos.photo.length - 1].dateupload) - (res.photos.photo[res.photos.photo.length - 1].dateupload == res.photos.photo[0].dateupload ? 1 : 0)
63
- : null;
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
- nextPluginOptionsDelta.push({
83
- 'DateMax': next_max_date,
84
- 'DateMin': pluginOptions.DateMin,
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,