splatone 0.0.8 → 0.0.9

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
@@ -12,6 +12,9 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
12
12
  - Marker Cluster: 密集しているジオタグをクラスタリングしてまとめて表示する
13
13
 
14
14
  ## Change Log
15
+ ### v0.0.8 → v0.0.9
16
+
17
+ * クエリを時間方向でも分割し効率化しました。(使い方に変更はありません)
15
18
 
16
19
  ### v0.0.7 → v0.0.8
17
20
 
package/crawler.js CHANGED
@@ -28,7 +28,7 @@ import { hideBin } from 'yargs/helpers';
28
28
  // -------------------------------
29
29
  import { loadPlugins } from './lib/pluginLoader.js';
30
30
  import paletteGenerator from './lib/paletteGenerator.js';
31
- import { dfsObject, bboxSize, saveGeoJsonObjectAsStream } from './lib/splatone.js';
31
+ import { dfsObject, bboxSize, saveGeoJsonObjectAsStream, buildPluginsOptions, loadAPIKey } from '#lib/splatone';
32
32
 
33
33
  const __filename = fileURLToPath(import.meta.url);
34
34
  const __dirname = dirname(__filename);
@@ -36,6 +36,7 @@ const VIZ_BASE = resolve(__dirname, "visualizer");
36
36
  const app = express();
37
37
  const port = 3000;
38
38
  const title = 'Splatone - Multi-Layer Composite Heatmap Viewer';
39
+ let pluginsOptions = {};
39
40
 
40
41
  try {
41
42
 
@@ -164,6 +165,9 @@ try {
164
165
  catch (e) { throw new Error(`--${name}: JSON エラー: ${e.message}`); }
165
166
  })()
166
167
  });
168
+ plugins.list().forEach(async (plug) => {
169
+ yargv = await plugins.call(plug, "yargv", yargv);
170
+ })
167
171
  Object.keys(all_visualizers).forEach((vis) => {
168
172
  yargv = yargv.option('vis-' + vis, {
169
173
  group: 'Visualization (最低一つの指定が必須です)',
@@ -172,7 +176,7 @@ try {
172
176
  description: all_visualizers[vis].description
173
177
  })
174
178
  });
175
- yargv = yargv.check((argv, options) => {
179
+ yargv = yargv.check(async (argv, options) => {
176
180
  if (Object.keys(all_visualizers).filter(v => argv["vis-" + v]).length == 0) {
177
181
  throw new Error('可視化ツールの指定がありません。最低一つは指定してください。');
178
182
  }
@@ -180,15 +184,18 @@ try {
180
184
  console.warn("--filedと--choppedが両方指定されています。--filedが優先されます。");
181
185
  argv.chopped = false;
182
186
  }
187
+ pluginsOptions = buildPluginsOptions(argv, plugins.list())
188
+ pluginsOptions[argv.plugin] = await plugins.call(argv.plugin, 'check', pluginsOptions[argv.plugin]);
183
189
  return true;
184
190
  });
185
191
  const argv = await yargv.parseAsync();
192
+
186
193
  const visualizers = {};
187
194
  for (const vis of Object.keys(all_visualizers).filter(v => argv[`vis-${v}`])) {
188
195
  visualizers[vis] = new all_visualizers[vis]();
189
196
  }
190
197
 
191
- const plugin_options = argv.options?.[argv.plugin] ?? {}
198
+ /* const plugin_options = argv.options?.[argv.plugin] ?? {}
192
199
  try {
193
200
  plugin_options.API_KEY = await loadAPIKey("flickr") ?? plugin_options.API_KEY;
194
201
  } catch (e) {
@@ -196,48 +203,15 @@ try {
196
203
  console.error("Error loading API key:", e.message);
197
204
  }
198
205
  //Nothing to do
199
- }
200
- await plugins.call(argv.plugin, 'init', plugin_options);
206
+ }*/
207
+ //pluginsOptions = buildPluginsOptions(argv, plugins.list());
208
+ await plugins.call(argv.plugin, 'init', pluginsOptions[argv.plugin]);
201
209
  if (argv.debugVerbose) {
202
210
  console.table([["Visualizer", Object.keys(visualizers)], ["Plugin", argv.plugin]]);
203
211
  }
204
212
 
205
- /* API Key読み込み */
206
- async function loadAPIKey(plugin = 'flickr') {
207
- //ファイルチェック→環境変数チェック
208
-
209
- const filePath = ".API_KEY." + plugin;
210
- const file = resolve(filePath);
211
- // 存在&読取権限チェック
212
- let key = null;
213
- try {
214
- await access(file, constants.F_OK | constants.R_OK);
215
- // 読み込み & トリム
216
- const raw = await readFile(file, 'utf8');
217
- //console.log(`[API KEY (${plugin}})] Read from FILE`);
218
- key = raw.trim();
219
- } catch (err) {
220
- if (Object.prototype.hasOwnProperty.call(process.env, "API_KEY_" + plugin)) {
221
- //console.log(`[API KEY (${plugin}})] Read from ENV`);
222
- key = process.env["API_KEY_" + plugin] ?? null;
223
- } else {
224
- const code = /** @type {{ code?: string }} */(err).code || 'UNKNOWN';
225
- throw new Error(`APIキーのファイルもしくは環境変数にアクセスできません: ${file} (code=${code})`);
226
- }
227
- }
228
- if (!key) {
229
- throw new Error(`APIキーのファイルが空です: ${file}`);
230
- }
231
- // ※任意: Flickr APIキーの緩い形式チェック(英数32+文字)
232
- // 公式に厳格仕様が明示されていないため、緩めのガードに留めます。
233
- if (!/^[A-Za-z0-9]{32,}$/.test(key)) {
234
- // 形式が怪しい場合は警告だけにするなら console.warn に変更
235
- throw new Error(`APIキーの形式が不正の可能性があります(英数字32文字以上を想定): ${file}`);
236
- }
237
- return key;
238
- }
239
-
240
213
 
214
+ const processing = {};
241
215
  const crawlers = {};
242
216
  const targets = {};
243
217
  // 初期中心(凱旋門)
@@ -267,6 +241,9 @@ try {
267
241
  return k1 < k2 ? k1 : k2;
268
242
  }
269
243
 
244
+ function unixTimeLocal(year, month, day, hour = 0, minute = 0, second = 0) {
245
+ return Math.round(new Date(year, month - 1, day, hour, minute, second).getTime() / 1000);
246
+ }
270
247
 
271
248
  function defaultMaxUploadTime(date = new Date()) {
272
249
  return Math.floor(date / 1000) - 360;
@@ -328,20 +305,21 @@ try {
328
305
  socket.emit("welcome", { socketId: socket.id, sessionId: sessionId, time: Date.now(), visualizers: Object.keys(visualizers) });
329
306
  //クローリング開始
330
307
  socket.on("crawling", async (req) => {
308
+
331
309
  try {
332
310
  if (sessionId !== req.sessionId) {
333
311
  console.warn("invalid sessionId:", req.sessionId);
334
312
  return;
335
313
  }
336
-
337
- await plugins.call('flickr', 'crawl', {
314
+ const optPlugin = {
338
315
  hexGrid: targets[req.sessionId].hex,
339
316
  triangles: targets[req.sessionId].triangles,
340
317
  sessionId: req.sessionId,
341
- //tags: targets[req.sessionId].tags,
342
318
  categories: targets[req.sessionId].categories,
343
- max_upload_date: defaultMaxUploadTime(),
344
- });
319
+ pluginOptions: pluginsOptions[argv.plugin]
320
+ };
321
+ //console.log(optPlugin);
322
+ await plugins.call(argv.plugin, 'crawl', optPlugin);
345
323
  }
346
324
  catch (e) {
347
325
  console.error(e);
@@ -352,7 +330,6 @@ try {
352
330
  // クロール範囲指定
353
331
  socket.on("target", (req) => {
354
332
  try {
355
- //console.log("target:", req);
356
333
  if (sessionId !== req.sessionId) {
357
334
  console.warn("invalid sessionId:", req.sessionId);
358
335
  return;
@@ -365,11 +342,13 @@ try {
365
342
  const { width, height } = bboxSize(boundary, units);
366
343
  //console.log("","w=",width,"/\th=",height);
367
344
  cellSize = Math.max(width / (3 * 30), height / (30 * Math.sqrt(3)));
368
- if(cellSize==0){
369
- cellSize=1;
345
+ if (cellSize == 0) {
346
+ cellSize = 1;
370
347
  }
371
348
  const msg = "セルサイズを[ " + cellSize + ' ' + units + " ]に設定しました。";
372
- console.log(msg)
349
+ if (argv.debugVerbose) {
350
+ console.log("INFO:", msg)
351
+ }
373
352
  io.to(sessionId).timeout(5000).emit('toast', {
374
353
  text: msg,
375
354
  class: "info"
@@ -510,7 +489,7 @@ try {
510
489
 
511
490
  //console.log(JSON.stringify(hexFC, null, 2));
512
491
  //res.json({ hex: hexFC, triangles: trianglesFC });
513
- targets[sessionId] = { hex: hexFC, triangles: trianglesFC, categories, splatonePalette };
492
+ targets[sessionId] = { hex: hexFC, triangles: trianglesFC, categories, splatonePalette, };
514
493
  socket.emit("hexgrid", { hex: hexFC, triangles: trianglesFC });
515
494
  } catch (e) {
516
495
  console.error(e);
@@ -584,7 +563,9 @@ try {
584
563
  });
585
564
  await subscribe('splatone:start', async p => {
586
565
  //console.log('[splatone:start]', p);
587
- const rtn = await runTask(p.plugin, p);
566
+ processing[p.sessionId] = (processing[p.sessionId] ?? 0) + 1;
567
+ let rtn = await runTask(p.plugin, p);
568
+ processing[p.sessionId]--;
588
569
  //console.log('[splatone:done]', p.plugin, rtn.photos.features.length,"photos are collected in hex",rtn.hexId,"tags:",rtn.tags,"final:",rtn.final);
589
570
  crawlers[p.sessionId][rtn.hexId] ??= {};
590
571
  crawlers[p.sessionId][rtn.hexId][rtn.category] ??= { items: featureCollection([]) };
@@ -595,20 +576,25 @@ try {
595
576
  crawlers[p.sessionId][rtn.hexId][rtn.category].crawled ??= 0;
596
577
  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;
597
578
  crawlers[p.sessionId][rtn.hexId][rtn.category].crawled = crawlers[p.sessionId][rtn.hexId][rtn.category].ids.size;
579
+
598
580
  if (argv.debugVerbose) {
599
- console.log(`(CRAWL) ${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}`);
581
+ 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}`);
600
582
  }
601
583
  const photos = featureCollection(rtn.photos.features.filter((f) => !duplicates.has(f.properties.id)));
602
584
  crawlers[p.sessionId][rtn.hexId][rtn.category].items
603
585
  = concatFC(crawlers[p.sessionId][rtn.hexId][rtn.category].items, photos);
586
+
604
587
  const { stats, progress, finish } = statsItems(crawlers[p.sessionId], targets[p.sessionId]);
605
588
  io.to(p.sessionId).emit('progress', { hexId: rtn.hexId, progress });
606
589
  if (!rtn.final) {
607
590
  // 次回クロール用に更新
608
- p.max_upload_date = rtn.next_max_upload_date;
609
- //console.log("next max_upload_date:", p.max_upload_date);
610
- api.emit('splatone:start', p);
611
- } else if (finish) {
591
+ rtn.nextPluginOptions.forEach((nextPluginOptions) => {
592
+ const p_clone = structuredClone(p);
593
+ p_clone.pluginOptions = nextPluginOptions
594
+ api.emit('splatone:start', p_clone);
595
+ });
596
+ //} else if (finish) {
597
+ } else if (processing[p.sessionId] == 0) {
612
598
  if (argv.debugVerbose) {
613
599
  console.table(stats);
614
600
  }
@@ -620,10 +606,9 @@ try {
620
606
  const resultId = uniqid();
621
607
  const result = crawlers[p.sessionId];
622
608
  const target = targets[p.sessionId];
623
-
624
609
  let geoJson = Object.fromEntries(Object.entries(visualizers).map(([vis, v]) => [vis, v.getFutureCollection(result, target)]));
625
610
 
626
- console.log('[splatone:finish]');
611
+ //console.log('[splatone:finish]');
627
612
  try {
628
613
  if (argv.chopped || argv.filed) {
629
614
  throw new RangeError("Invalid string length");
@@ -723,6 +708,6 @@ try {
723
708
 
724
709
  process.on('SIGINT', async () => { await plugins.stopAll(); process.exit(0); });
725
710
  } catch (e) {
711
+ console.error("[SPLATONE ERROR]");
726
712
  console.error(e);
727
-
728
713
  }
package/lib/PluginBase.js CHANGED
@@ -12,12 +12,27 @@ export class PluginBase {
12
12
  this.api = api;
13
13
  this.options = options;
14
14
  }
15
+
16
+ argKey(key) {
17
+ return "p-" + this.id + "-" + key;
18
+ }
19
+ async check(option) {
20
+ //throw Error("Plugin Option Error!");
21
+ return true;
22
+ }
23
+
15
24
  async init(options = {}) {
16
25
  Object.assign(this.options, options);
17
26
  }
27
+
28
+ async options(yargs) {
29
+
30
+ return yargs;
31
+ }
32
+
18
33
  async start() {
19
34
  this.started = true;
20
35
  //this.api.log(`[${this.constructor.id}] start`);
21
36
  }
22
- async stop() {}
37
+ async stop() { }
23
38
  }
@@ -107,9 +107,9 @@ async function resolveEntry(folder) {
107
107
  function validateClass(PluginClass, file) {
108
108
  if (typeof PluginClass !== 'function') throw new Error(`Plugin must export a class: ${file}`);
109
109
  // 静的メタ
110
- if (!PluginClass.id || typeof PluginClass.id !== 'string') {
110
+ /*if (!PluginClass.id || typeof PluginClass.id !== 'string') {
111
111
  throw new Error(`static id (string) is required: ${file}`);
112
- }
112
+ }*/ //動的に決定する(ディレクトリ名)
113
113
  if (!PluginClass.version || typeof PluginClass.version !== 'string') {
114
114
  throw new Error(`static version (string) is required: ${file}`);
115
115
  }
package/lib/splatone.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // @turf/turf v6/v7 どちらでもOK(rhumbDistanceはv7で統合済み)
3
3
  import { point, distance, rhumbDistance, bbox as turfBbox } from '@turf/turf';
4
4
  import { createWriteStream } from 'node:fs';
5
- import { mkdir } from 'node:fs/promises';
5
+ import { mkdir, constants, access, readFile } from 'node:fs/promises';
6
6
  import path from 'node:path';
7
7
  import { pipeline as pipelineCb } from 'node:stream';
8
8
  import { promisify } from 'node:util';
@@ -145,7 +145,94 @@ export async function saveGeoJsonObjectAsStream(geoJsonObject, outfile) {
145
145
  await pipeline(src, dest);
146
146
  return destPath;
147
147
  }
148
+
149
+ export function buildPluginsOptions(argv, pluginIds) {
150
+ const out = {};
151
+ for (const id of pluginIds) {
152
+ const prefix = `p-${id}`;
153
+ const opts = {};
154
+ for (const [key, val] of Object.entries(argv)) {
155
+ if (key === '_' || key === '$0') continue;
156
+ if (key === prefix) { // --p-id=VALUE
157
+ opts.__value = val;
158
+ continue;
159
+ }
160
+ if (key.startsWith(prefix)) { // --p-id.xxx or --p-id-xxx
161
+ const sep = key[prefix.length];
162
+ if (sep === '.' || sep === '-') {
163
+ let sub = key.slice(prefix.length + 1).replace(/-/g, '.');
164
+ if (sub) setDeep(opts, sub, val);
165
+ }
166
+ }
167
+ }
168
+ if (Object.keys(opts).length) out[id] = opts;
169
+ }
170
+ //return out;
171
+ const lowerRe = /^\p{Ll}/u;
172
+
173
+ const out2 = {};
174
+ for (const [k, v] of Object.entries(out)) {
175
+ if (v && typeof v === "object" && !Array.isArray(v)) {
176
+ // 子がプレーンオブジェクトの場合だけ二階層目をフィルタ
177
+ out2[k] = Object.fromEntries(
178
+ Object.entries(v).filter(([kk]) => kk !== "" && !lowerRe.test(kk))
179
+ );
180
+ } else {
181
+ // それ以外(配列・null・プリミティブ)はそのまま
182
+ out2[k] = v;
183
+ }
184
+ }
185
+ return out2;
186
+ }
187
+
188
+ function setDeep(obj, path, value) {
189
+ const segs = path.split('.');
190
+ let cur = obj;
191
+ for (let i = 0; i < segs.length - 1; i++) {
192
+ const k = segs[i];
193
+ cur = (cur[k] ??= {});
194
+ }
195
+ cur[segs[segs.length - 1]] = value;
196
+ }
197
+
198
+ /* API Key読み込み */
199
+ export async function loadAPIKey(plugin = 'flickr') {
200
+ //ファイルチェック→環境変数チェック
201
+
202
+ const filePath = ".API_KEY." + plugin;
203
+ const file = path.resolve(filePath);
204
+ //console.log(file);
205
+ // 存在&読取権限チェック
206
+ let key = null;
207
+ try {
208
+ await access(file, constants.F_OK | constants.R_OK);
209
+ // 読み込み & トリム
210
+ const raw = await readFile(file, 'utf8');
211
+ //console.log(`[API KEY (${plugin}})] Read from FILE`);
212
+ key = raw.trim();
213
+ } catch (err) {
214
+ if (Object.prototype.hasOwnProperty.call(process.env, "API_KEY_" + plugin)) {
215
+ //console.log(`[API KEY (${plugin}})] Read from ENV`);
216
+ key = process.env["API_KEY_" + plugin] ?? null;
217
+ } else {
218
+ const code = /** @type {{ code?: string }} */(err).code || 'UNKNOWN';
219
+ throw new Error(`APIキーのファイルもしくは環境変数にアクセスできません: ${file} (msg=${err.message})`);
220
+ }
221
+ }
222
+ if (!key) {
223
+ throw new Error(`APIキーのファイルが空です: ${file}`);
224
+ }
225
+ // ※任意: Flickr APIキーの緩い形式チェック(英数32+文字)
226
+ // 公式に厳格仕様が明示されていないため、緩めのガードに留めます。
227
+ if (!/^[A-Za-z0-9]{32,}$/.test(key)) {
228
+ // 形式が怪しい場合は警告だけにするなら console.warn に変更
229
+ throw new Error(`APIキーの形式が不正の可能性があります(英数字32文字以上を想定): ${file}`);
230
+ }
231
+ return key;
232
+ }
233
+
234
+ /*
148
235
  export default {
149
- dfsObject, bboxSize, saveGeoJsonObjectAsStream
150
- };
236
+ dfsObject, bboxSize, saveGeoJsonObjectAsStream, buildPluginsOptions, loadAPIKey
237
+ };*/
151
238
  //const {width,height} = bboxSize(b, "kilometers", "geodesic");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "splatone",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Multi-layer Composite Heatmap",
5
5
  "homepage": "https://github.com/YokoyamaLab/Splatone#readme",
6
6
  "bugs": {
@@ -17,6 +17,9 @@
17
17
  "bin": {
18
18
  "crawler": "./crawler.js"
19
19
  },
20
+ "imports": {
21
+ "#lib/*": "./lib/*.js"
22
+ },
20
23
  "scripts": {
21
24
  "test": "echo \"Error: no test specified\" && exit 1"
22
25
  },
@@ -1,23 +1,102 @@
1
- // plugins/hello/index.js
1
+ // plugins/flickr/index.js
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
2
4
  import { PluginBase } from '../../lib/PluginBase.js';
3
5
  import { bbox, polygon, centroid, booleanPointInPolygon, featureCollection } from '@turf/turf';
4
-
6
+ import { loadAPIKey } from '#lib/splatone';
5
7
  export default class FlickrPlugin extends PluginBase {
6
- static id = 'flickr'; // 必須
7
- static name = 'Flickr Plugin'; // 任意
8
+
9
+ static name = 'Flickr Plugin'; // 任意
8
10
  static description = 'Flickrからジオタグ付きデータを収集する。';
9
11
  static version = '1.0.0';
12
+ constructor(api, options = {}) {
13
+ super(api, options);
14
+ this.id = path.basename(path.dirname(fileURLToPath(import.meta.url)));//必須(ディレクトリ名がプラグイン名)
15
+ }
16
+ async yargv(yargv) {
17
+ // 必須項目にすると、このプラグインを使用しない時も必須になります。
18
+ // 必須項目は作らず、initで例外を投げてください。
19
+ return yargv.option(this.argKey('APIKEY'), {
20
+ group: 'For ' + this.id + ' Plugin',
21
+ type: 'string',
22
+ description: 'Flickr ServiceのAPI KEY'
23
+ }).coerce(this.argKey('APIKEY'), opt => {
24
+ return opt
25
+ }).option(this.argKey('DateMax'), {
26
+ group: 'For ' + this.id + ' Plugin',
27
+ type: 'string',
28
+ default: Math.floor(new Date() / 1000) - 360,
29
+ description: 'クローリング期間(最大) UNIX TIMEもしくはYYYY-MM-DD'
30
+ }).coerce(this.argKey('DateMax'), opt => {
31
+ if (!opt) return opt; // undefined/null はそのまま返す
32
+
33
+ // YYYY-MM-DD 形式のチェック
34
+ const dateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(opt);
35
+ if (dateMatch) {
36
+ const [_, year, month, day] = dateMatch;
37
+ const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
38
+ if (date.toString() === 'Invalid Date') {
39
+ throw new Error(`Invalid date format: ${opt} (正しい日付を指定してください)`);
40
+ }
41
+ return Math.floor(date.getTime() / 1000);
42
+ }
43
+
44
+ // 数値文字列または数値のチェック
45
+ const num = Number(opt);
46
+ if (Number.isFinite(num)) {
47
+ return Math.floor(num); // 確実に整数に
48
+ }
49
+
50
+ throw new Error(`Invalid date/time format: ${opt} (YYYY-MM-DD または UNIX時間(秒)で指定してください)`);
51
+ }).option(this.argKey('DateMin'), {
52
+ group: 'For ' + this.id + ' Plugin',
53
+ type: 'string',
54
+ default: Math.round(new Date(2004, 1 - 1, 1, 0, 0, 0).getTime() / 1000),
55
+ description: 'クローリング期間(最小) UNIX TIMEもしくはYYYY-MM-DD'
56
+ }).coerce(this.argKey('DateMin'), opt => {
57
+ if (!opt) return opt; // undefined/null はそのまま返す
58
+
59
+ // YYYY-MM-DD 形式のチェック
60
+ const dateMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(opt);
61
+ if (dateMatch) {
62
+ const [_, year, month, day] = dateMatch;
63
+ const date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day));
64
+ if (date.toString() === 'Invalid Date') {
65
+ throw new Error(`Invalid date format: ${opt} (正しい日付を指定してください)`);
66
+ }
67
+ return Math.floor(date.getTime() / 1000);
68
+ }
69
+
70
+ // 数値文字列または数値のチェック
71
+ const num = Number(opt);
72
+ if (Number.isFinite(num)) {
73
+ return Math.floor(num); // 確実に整数に
74
+ }
75
+ throw new Error(`Invalid date/time format: ${opt} (YYYY-MM-DD または UNIX時間(秒)で指定してください)`)
76
+ });
77
+ }
78
+
79
+ async check(options) {
80
+ const RE_FLICKR_API_KEY = /^[0-9a-f]{32}$/i;
81
+ if (!options['APIKEY']) {
82
+ const apikey = await loadAPIKey(this.id);
83
+ //console.log(apikey);
84
+ options['APIKEY'] = apikey;
85
+ } else if (!RE_FLICKR_API_KEY.test(options['APIKEY'])) {
86
+ throw new Error('Invalid Flickr API key format: 32桁 16進数で指定してください');
87
+ }
88
+ return options;
89
+ }
10
90
 
11
91
  async stop() {
12
92
  //this.api.log(`[${this.constructor.id}] stop`);
13
93
  }
14
94
 
15
95
  // 任意の公開メソッド
16
- async crawl({ hexGrid, triangles/*, tags*/, categories, max_upload_date, sessionId }) {
96
+ async crawl({ hexGrid, triangles/*, tags*/, categories, sessionId, pluginOptions }) {
17
97
  if (!this.started) {
18
98
  this.start();
19
99
  }
20
-
21
100
  const getTrianglesInHex = (hex, triangles) => {
22
101
  const hexPoly = polygon(hex.geometry.coordinates);
23
102
  const selected = triangles.features.filter(tri => {
@@ -38,18 +117,18 @@ export default class FlickrPlugin extends PluginBase {
38
117
  //console.log("tag=",ck,"/",tags);
39
118
  hexQuery[item.properties.hexId][ck] = { photos: [], tags, final: false };
40
119
  this.api.emit('splatone:start', {
41
- plugin: 'flickr',
42
- API_KEY: this.options.API_KEY,
120
+ plugin: this.id,
121
+ //API_KEY: this.APIKEY ?? pluginOptions.APIKEY,
43
122
  hex: item,
44
123
  triangles: getTrianglesInHex(item, triangles),
45
124
  bbox: bbox(item.geometry),
46
125
  category: ck,
47
126
  tags,
48
- max_upload_date,
127
+ pluginOptions,
49
128
  sessionId
50
129
  });
51
130
  }));
52
131
  }));
53
- return `Flickr, ${this.options.API_KEY}, ${hexGrid.features.length} bboxes processed.`;
132
+ return `${this.id}, ${this.options.API_KEY}, ${hexGrid.features.length} bboxes processed.`;
54
133
  }
55
134
  }
@@ -3,21 +3,23 @@ import { point, featureCollection } from "@turf/helpers";
3
3
  import booleanPointInPolygon from "@turf/boolean-point-in-polygon";
4
4
 
5
5
  export default async function ({
6
- API_KEY = "",
6
+ //API_KEY = "",
7
7
  bbox = [0, 0, 0, 0],
8
8
  tags = "",
9
9
  category = "",
10
- max_upload_date = null,
11
10
  hex = null,
12
11
  triangles = null,
12
+ pluginOptions
13
13
  }) {
14
- const { flickr } = createFlickr(API_KEY);
14
+ //console.log("{PLUGIN}", pluginOptions);
15
+ const { flickr } = createFlickr(pluginOptions["APIKEY"]);
15
16
  const baseParams = {
16
17
  bbox: bbox.join(','),
17
18
  tags: tags,
18
- max_upload_date: max_upload_date,
19
-
19
+ max_upload_date: pluginOptions["DateMax"],
20
+ min_upload_date: pluginOptions["DateMin"],
20
21
  };
22
+ //console.log("[baseParams]",baseParams);
21
23
  const res = await flickr("flickr.photos.search", {
22
24
  ...baseParams,
23
25
  has_geo: 1,
@@ -27,7 +29,6 @@ export default async function ({
27
29
  sort: "date-posted-desc"
28
30
  });
29
31
  //console.log(baseParams);
30
- //console.log("[(Crawl)", hex.properties.hexId, category, "]", (new Date(max_upload_date * 1000)).toLocaleString(), "-> photos:", res.photos.photo.length, "/", res.photos.total);
31
32
  const ids = [];
32
33
  const authors = {};
33
34
  const photos = featureCollection(res.photos.photo.filter(photo => {
@@ -54,7 +55,9 @@ export default async function ({
54
55
  }));
55
56
  const outside = res.photos.photo.length - photos.features.length;
56
57
  //console.log(JSON.stringify(photos, null, 4));
57
- let next_max_upload_date
58
+
59
+ const nextPluginOptionsDelta = [];
60
+ let next_max_date
58
61
  = res.photos.photo.length > 0
59
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)
60
63
  : null;
@@ -62,17 +65,47 @@ export default async function ({
62
65
  if (Object.keys(authors).length == 1 && window < 60 * 60) {
63
66
  const skip = window < 5 ? 0.1 : 12;
64
67
  console.warn("[Warning]", `High posting activity detected for ${Object.keys(authors)} within ${window} s. the crawler will skip the next ${skip} hours.`);
65
- next_max_upload_date -= 60 * 60 * skip;
68
+ next_max_date -= 60 * 60 * skip;
69
+ }
70
+ if (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
+ });
81
+ } else {
82
+ nextPluginOptionsDelta.push({
83
+ 'DateMax': next_max_date,
84
+ 'DateMin': pluginOptions.DateMin
85
+ });
66
86
  }
67
87
  return {
68
88
  photos,
69
89
  hexId: hex.properties.hexId,
70
90
  tags,
71
91
  category,
72
- next_max_upload_date,
92
+ nextPluginOptions: nextPluginOptionsDelta.map(e => { return { ...pluginOptions, ...e } }),
73
93
  total: res.photos.total,
74
94
  outside: outside,
75
95
  ids,
76
96
  final: res.photos.photo.length == res.photos.total
77
97
  };
98
+ /*
99
+ pluginOptions["DateMax"] = next_max_date;
100
+ return {
101
+ photos,
102
+ hexId: hex.properties.hexId,
103
+ tags,
104
+ category,
105
+ nextPluginOptions: pluginOptions,
106
+ total: res.photos.total,
107
+ outside: outside,
108
+ ids,
109
+ final: res.photos.photo.length == res.photos.total
110
+ };*/
78
111
  }
package/views/index.ejs CHANGED
@@ -347,7 +347,7 @@
347
347
  overlays[`[${vis}]`] = layers;
348
348
  } else {
349
349
  for (const name in layers) {
350
- console.log("Lay", vis, name);
350
+ //console.log("Lay", vis, name);
351
351
  overlays[`[${vis}] ${name}`] = layers[name];
352
352
  }
353
353
  }
@@ -1,11 +1,11 @@
1
1
  let booted = false;
2
2
  export default async function main(map, geojson, options = {}) {
3
- console.log("main");
3
+ //console.log("main");
4
4
  if (booted) return;
5
5
  booted = true;
6
6
  const layers = {};
7
7
  for (const cat in geojson) {
8
- console.log(cat);
8
+ //console.log(cat);
9
9
  const layer = addGeoJSONLayer(map, geojson[cat], {
10
10
  pointToLayer: (feature, latlng) => {
11
11
  return L.circleMarker(latlng, {
@@ -125,7 +125,7 @@ export default async function main(map, geojson, options = { palette: {} }) {
125
125
 
126
126
  // まだマップに載っていないクラスターは載せる
127
127
  for (const [cat, grp] of clusterByCategory.entries()) {
128
- console.log("add map", cat);
128
+ //console.log("add map", cat);
129
129
  if (!map.hasLayer(grp)) grp.addTo(map);
130
130
  }
131
131
 
@@ -157,7 +157,7 @@ export default async function main(map, geojson, options = { palette: {} }) {
157
157
 
158
158
  // 実データを投入(fetchで置き換え可)
159
159
  for (const cat in geojson) {
160
- console.log(cat);
160
+ //console.log(cat);
161
161
  addFeatureCollectionByCategory(geojson[cat], cat);
162
162
  }
163
163
  // Mapから単なるObjectへ