splatone 0.0.7 → 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 +9 -0
- package/crawler.js +45 -60
- package/lib/PluginBase.js +16 -1
- package/lib/pluginLoader.js +2 -2
- package/lib/splatone.js +90 -3
- package/package.json +4 -1
- package/plugins/flickr/index.js +89 -10
- package/plugins/flickr/worker.js +42 -9
- package/views/index.ejs +2 -1
- package/visualizer/bulky/web.js +2 -2
- package/visualizer/marker-cluster/web.js +2 -2
package/README.md
CHANGED
|
@@ -12,6 +12,15 @@ SNSのジオタグ付きポストをキーワードに基づいて収集する
|
|
|
12
12
|
- Marker Cluster: 密集しているジオタグをクラスタリングしてまとめて表示する
|
|
13
13
|
|
|
14
14
|
## Change Log
|
|
15
|
+
### v0.0.8 → v0.0.9
|
|
16
|
+
|
|
17
|
+
* クエリを時間方向でも分割し効率化しました。(使い方に変更はありません)
|
|
18
|
+
|
|
19
|
+
### v0.0.7 → v0.0.8
|
|
20
|
+
|
|
21
|
+
* 範囲指定とHexGridの表示・非表示ができるようになりました。
|
|
22
|
+
* デフォルトで非表示
|
|
23
|
+
* 表示したい場合はレイヤコントロールにて切り替えてください
|
|
15
24
|
|
|
16
25
|
### v0.0.6 → v0.0.7
|
|
17
26
|
|
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 '
|
|
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
|
|
|
@@ -108,7 +109,7 @@ try {
|
|
|
108
109
|
// コマンド例
|
|
109
110
|
// 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
|
|
110
111
|
// 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
|
|
111
|
-
// node crawler.js -p flickr -k "水辺=sea,ocean,beach
|
|
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
|
|
112
113
|
let yargv = await yargs(hideBin(process.argv))
|
|
113
114
|
.strict() // 未定義オプションはエラー
|
|
114
115
|
.usage('使い方: $0 [options]')
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(`
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
}
|
package/lib/pluginLoader.js
CHANGED
|
@@ -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.
|
|
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
|
},
|
package/plugins/flickr/index.js
CHANGED
|
@@ -1,23 +1,102 @@
|
|
|
1
|
-
// plugins/
|
|
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
|
-
|
|
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,
|
|
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:
|
|
42
|
-
API_KEY: this.
|
|
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
|
-
|
|
127
|
+
pluginOptions,
|
|
49
128
|
sessionId
|
|
50
129
|
});
|
|
51
130
|
}));
|
|
52
131
|
}));
|
|
53
|
-
return
|
|
132
|
+
return `${this.id}, ${this.options.API_KEY}, ${hexGrid.features.length} bboxes processed.`;
|
|
54
133
|
}
|
|
55
134
|
}
|
package/plugins/flickr/worker.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -370,6 +370,7 @@
|
|
|
370
370
|
//console.log(overlays);
|
|
371
371
|
layerControl = L.control.layers([baseLayer], {
|
|
372
372
|
...overlays,
|
|
373
|
+
"Boundary": drawnItems,
|
|
373
374
|
"Hex Grid": hexLayer,
|
|
374
375
|
"Tri Grid": triLayer
|
|
375
376
|
}, {
|
package/visualizer/bulky/web.js
CHANGED
|
@@ -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へ
|