arcgis-core-utilities について
ArcGIS JavaScript SDKのコアユーティリティの利用パターンと例をまとめたスキルです。リアクティブな状態、コレクション、非同期処理、ワーカー、リクエスト、ローカライズを扱うArcGIS開発者向けです。
主な機能
- 01Accessorクラスとプロパティのパターン
- 02リアクティブな監視とイベント処理
- 03変更通知付きCollectionの利用
- 04Promiseとキャンセル用ユーティリティ
- 05バックグラウンドワーカー実行
- 06リクエストとローカライズ
活用シーン
- 01ArcGISのビューやレイヤー状態の監視
- 02コレクション変更イベントの管理
- 03ワーカーでの重い処理の実行
- 04キャンセル可能なArcGISリクエストの処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ saschabrunnerch/ arcgis-maps-sdk-js-ai-context --skill arcgis-core-utilitiesインストール後、Claude Code で/arcgis-core-utilitiesを入力して読み込まれたか確認します。
詳細
ArcGIS コアユーティリティ
Accessor、Collection、リアクティブユーティリティ、ワーカーなどのコアインフラストラクチャのパターンには、このスキルを使用してください。
インポートパターン
ESM の直接インポート
import * as reactiveUtils from "@arcgis/core/core/reactiveUtils.js";
import * as promiseUtils from "@arcgis/core/core/promiseUtils.js";
import Collection from "@arcgis/core/core/Collection.js";
import Handles from "@arcgis/core/core/Handles.js";
動的インポート(CDN)
const reactiveUtils = await $arcgis.import(
"@arcgis/core/core/reactiveUtils.js",
);
const promiseUtils = await $arcgis.import("@arcgis/core/core/promiseUtils.js");
Accessor パターン
Accessor クラスは、プロパティ監視と計算プロパティを提供し、すべての ArcGIS クラスの基盤となります。
カスタム Accessor クラスの作成
import Accessor from "@arcgis/core/core/Accessor.js";
import {
subclass,
property,
} from "@arcgis/core/core/accessorSupport/decorators.js";
@subclass("myapp.CustomClass")
class CustomClass extends Accessor {
@property()
name = "default";
@property()
value = 0;
// Computed property
@property({
readOnly: true,
dependsOn: ["name", "value"],
})
get summary() {
return `${this.name}: ${this.value}`;
}
}
const instance = new CustomClass({ name: "Test", value: 42 });
console.log(instance.summary); // "Test: 42"
プロパティデコレーター
@subclass("myapp.MyClass")
class MyClass extends Accessor {
// Basic property
@property()
title = "";
// Property with type casting
@property({ type: Number })
count = 0;
// Read-only computed property
@property({ readOnly: true })
get displayName() {
return this.title.toUpperCase();
}
// Property with custom setter
@property()
get status() {
return this._status;
}
set status(value) {
this._status = value?.toLowerCase() || "unknown";
}
// Property that depends on others
@property({
readOnly: true,
dependsOn: ["count", "title"],
})
get info() {
return `${this.title} (${this.count})`;
}
}
プロパティの型
@property({ type: String })
name = "";
@property({ type: Number })
count = 0;
@property({ type: Boolean })
enabled = true;
@property({ type: Date })
createdAt = new Date();
@property({ type: [Number] }) // Array of numbers
values = [];
@property({ type: SomeClass }) // Custom class
child = null;
Collection
Collection は、変更通知機能を備えた配列のようなクラスです。
基本的な使い方
import Collection from "@arcgis/core/core/Collection.js";
const collection = new Collection([
{ id: 1, name: "Item 1" },
{ id: 2, name: "Item 2" },
]);
// Add items
collection.add({ id: 3, name: "Item 3" });
collection.addMany([
{ id: 4, name: "Item 4" },
{ id: 5, name: "Item 5" },
]);
// Remove items
collection.remove(item);
collection.removeAt(0);
collection.removeMany([item1, item2]);
collection.removeAll();
// Access items
const first = collection.at(0);
const all = collection.toArray();
const length = collection.length;
Collection のメソッド
// Find items
const found = collection.find((item) => item.id === 3);
const index = collection.findIndex((item) => item.name === "Item 2");
const filtered = collection.filter((item) => item.value > 10);
// Transform
const mapped = collection.map((item) => item.name);
const reduced = collection.reduce((acc, item) => acc + item.value, 0);
// Check
const hasItem = collection.includes(item);
const exists = collection.some((item) => item.active);
const allActive = collection.every((item) => item.active);
// Sort and reorder
collection.sort((a, b) => a.name.localeCompare(b.name));
collection.reverse();
collection.reorder(item, 0); // Move item to index 0
// Stack operations
collection.push(item); // Add to end
collection.pop(); // Remove from end
collection.unshift(item); // Add to beginning
collection.shift(); // Remove from beginning
collection.splice(1, 2); // Remove 2 items at index 1
// Iterate
collection.forEach((item) => console.log(item.name));
for (const item of collection) {
console.log(item);
}
// Filter by type
const featureLayers = map.layers.ofType(FeatureLayer);
Collection のイベント
// Watch for changes
collection.on("change", (event) => {
console.log("Added:", event.added);
console.log("Removed:", event.removed);
console.log("Moved:", event.moved);
});
// Watch length
collection.watch("length", (newLength, oldLength) => {
console.log(`Length changed from ${oldLength} to ${newLength}`);
});
型付き Collection
// Collection of specific type
import Graphic from "@arcgis/core/Graphic.js";
const graphics = new Collection();
graphics.addMany([
new Graphic({ geometry: point1 }),
new Graphic({ geometry: point2 }),
]);
// Map's layers is a Collection
map.layers.add(featureLayer);
map.layers.reorder(featureLayer, 0);
reactiveUtils
プロパティや状態の変更を監視するための最新のリアクティブユーティリティです。
watch()
プロパティの変更を監視します。
import * as reactiveUtils from "@arcgis/core/core/reactiveUtils.js";
// Watch single property
const handle = reactiveUtils.watch(
() => view.scale,
(scale) => {
console.log("Scale changed to:", scale);
},
);
// With options
reactiveUtils.watch(
() => view.extent,
(extent) => console.log("Extent:", extent),
{ initial: true }, // Call immediately with current value
);
// Stop watching
handle.remove();
when()
条件が true になるまで待機します。
// Wait for view to be ready
await reactiveUtils.when(
() => view.ready,
() => console.log("View is ready!"),
);
// Wait for layer to load
await reactiveUtils.when(() => layer.loaded);
console.log("Layer loaded");
// With timeout (throws if not met)
await reactiveUtils.when(() => view.stationary, { timeout: 5000 });
once()
値の変更を一度だけ監視します。
// Execute once when condition is met
await reactiveUtils.once(() => view.ready);
console.log("View became ready");
// Once with callback
reactiveUtils.once(
() => layer.visible,
(visible) => console.log("Layer visibility changed to:", visible),
);
whenOnce()
プロパティが一度 truthy になるまで待機し、Promise を返します。
// Wait for view to be ready
await reactiveUtils.whenOnce(() => view.ready);
console.log("View is ready");
on()
リアクティブにイベントをリッスンします。
// Listen to click events
const handle = reactiveUtils.on(
() => view,
"click",
(event) => {
console.log("Clicked at:", event.mapPoint);
},
);
// Listen to layer view events
reactiveUtils.on(
() => view.whenLayerView(layer),
"highlight",
(event) => console.log("Highlight changed"),
);
複数のプロパティ
// Watch multiple properties
reactiveUtils.watch(
() => [view.center, view.zoom],
([center, zoom]) => {
console.log(`Center: ${center.x}, ${center.y}, Zoom: ${zoom}`);
},
);
// Computed expression
reactiveUtils.watch(
() => view.scale < 50000,
(isZoomedIn) => {
layer.visible = isZoomedIn;
},
);
Sync オプション
// Synchronous updates (use carefully)
reactiveUtils.watch(
() => view.extent,
(extent) => updateUI(extent),
{ sync: true },
);
ハンドル
クリーンアップのために複数の監視ハンドルを管理します。
import Handles from "@arcgis/core/core/Handles.js";
const handles = new Handles();
// Add handles
handles.add(
reactiveUtils.watch(
() => view.scale,
(scale) => {},
),
);
handles.add(
view.on("click", (e) => {}),
"click-handlers", // Group name
);
handles.add(
[
reactiveUtils.watch(
() => view.center,
() => {},
),
reactiveUtils.watch(
() => view.zoom,
() => {},
),
],
"view-watchers",
);
// Remove specific group
handles.remove("click-handlers");
// Check if group exists
handles.has("view-watchers"); // true
// Remove all
handles.removeAll();
// Destroy (also removes all)
handles.destroy();
カスタムクラスでの使用
@subclass("myapp.MyWidget")
class MyWidget extends Accessor {
constructor(props) {
super(props);
this.handles = new Handles();
}
initialize() {
this.handles.add(
reactiveUtils.watch(
() => this.view?.scale,
(scale) => this.onScaleChange(scale),
),
);
}
destroy() {
this.handles.destroy();
super.destroy();
}
}
Accessor の addHandles の使用
Accessor ベースのクラスには、ハンドル管理機能が組み込まれています。
// Add handles directly to an Accessor instance
view.addHandles(
reactiveUtils.watch(
() => view.scale,
(scale) => {},
),
"my-group",
);
// Check if handles exist
view.hasHandles("my-group"); // true
// Remove handles by group
view.removeHandles("my-group");
promiseUtils
Promise を扱うためのユーティリティです。
import * as promiseUtils from "@arcgis/core/core/promiseUtils.js";
eachAlways()
成功・失敗にかかわらず、すべての Promise が完了するまで待機します。
const results = await promiseUtils.eachAlways([
fetch("/api/data1"),
fetch("/api/data2"),
fetch("/api/data3"),
]);
results.forEach((result, index) => {
if (result.error) {
console.error(`Request ${index} failed:`, result.error);
} else {
console.log(`Request ${index} succeeded:`, result.value);
}
});
debounce()
関数の呼び出しをデバウンスします。
const debouncedSearch = promiseUtils.debounce(async (query) => {
const results = await searchService.search(query);
return results;
});
// Rapid calls will be debounced
input.addEventListener("input", async (e) => {
try {
const results = await debouncedSearch(e.target.value);
displayResults(results);
} catch (e) {
if (!promiseUtils.isAbortError(e)) {
console.error(e);
}
}
});
isAbortError()
エラーが中止された処理によるものかどうかを確認します。
try {
await someAsyncOperation();
} catch (error) {
if (promiseUtils.isAbortError(error)) {
console.log("Operation was cancelled");
} else {
throw error;
}
}
ignoreAbortErrors()
Promise からの中止エラーを抑制します。
// Instead of try/catch with isAbortError check
await promiseUtils.ignoreAbortErrors(debouncedFunction());
createAbortError()
AbortSignal とともに使用する中止エラーを作成します。
const error = promiseUtils.createAbortError();
// Useful in custom async operations that support cancellation
filter()
配列を非同期にフィルタリングします。
const validItems = await promiseUtils.filter(items, async (item) => {
const result = await validateItem(item);
return result.isValid;
});
ワーカー
バックグラウンドスレッドで負荷の高い計算を実行します。
import * as workers from "@arcgis/core/core/workers.js";
ワーカー接続を開く
const connection = await workers.open("path/to/worker.js");
// Execute method on worker
const result = await connection.invoke("methodName", { data: "params" });
// Close when done
connection.close();
ワーカースクリプトの例
// worker.js
define([], function () {
return {
methodName: function (params) {
// Heavy computation here
const result = processData(params.data);
return result;
},
anotherMethod: function (params) {
return params.value * 2;
},
};
});
負荷の高いタスクでのワーカーの使用
// Main script
import * as workers from "@arcgis/core/core/workers.js";
async function processLargeDataset(data) {
const connection = await workers.open("./dataProcessor.js");
try {
const result = await connection.invoke("process", {
data: data,
options: { threshold: 100 },
});
return result;
} finally {
connection.close();
}
}
エラー処理
import Error from "@arcgis/core/core/Error.js";
// Create custom error
const error = new Error("layer-load-error", "Failed to load layer", {
layer: layer,
originalError: e,
});
// Check error type
if (error.name === "layer-load-error") {
console.log("Layer failed:", error.details.layer.title);
}
URL ユーティリティ
import * as urlUtils from "@arcgis/core/core/urlUtils.js";
// Add proxy rule
urlUtils.addProxyRule({
urlPrefix: "https://services.arcgis.com",
proxyUrl: "/proxy",
});
esriRequest
SDK に同梱されている HTTP クライアントです。ArcGIS 認証、プロキシルール、CORS を自動的に処理します。ArcGIS REST エンドポイントにアクセスする場合や、SDK レベルの認証・プロキシ処理を利用したい一般的な HTTP 通信に使用してください。
基本的なリクエスト
import esriRequest from "@arcgis/core/request.js";
// GET request with JSON response
const response = await esriRequest(url, {
query: { f: "json" },
responseType: "json",
});
console.log("Status:", response.httpStatus);
console.log("Data:", response.data);
オプションを指定したリクエスト
const response = await esriRequest(url, {
query: {
f: "json",
param1: "value1",
},
responseType: "json", // "json" | "text" | "array-buffer" | "blob" | "image" | "document" | "xml"
method: "post", // "auto" | "head" | "post"
body: formData, // For POST requests
timeout: 30000, // Timeout in ms
headers: {
"X-Custom-Header": "value",
},
});
バイナリデータのダウンロード
// Image response
const imageResponse = await esriRequest(imageUrl, {
responseType: "image",
});
const imageElement = imageResponse.data;
// Binary data
const binaryResponse = await esriRequest(fileUrl, {
responseType: "array-buffer",
});
const arrayBuffer = binaryResponse.data;
中止可能なリクエスト
キャンセルには AbortController を組み合わせてください。特に、カスタムレイヤーやデバウンスを使ったワークフローでは重要です。ユーザーによるキャンセルと実際のエラーを区別するには、promiseUtils.isAbortError を使用してください。
import esriRequest from "@arcgis/core/request.js";
import * as promiseUtils from "@arcgis/core/core/promiseUtils.js";
const controller = new AbortController();
try {
const response = await esriRequest(url, {
responseType: "json",
signal: controller.signal,
});
} catch (error) {
if (promiseUtils.isAbortError(error)) {
// Cancelled — expected, do nothing
} else {
throw error;
}
}
// Cancel the in-flight request
controller.abort();
スケジューリング
import * as scheduling from "@arcgis/core/core/scheduling.js";
// Schedule for next frame
const handle = scheduling.addFrameTask({
update: (event) => {
// Called every frame
console.log("Delta time:", event.deltaTime);
console.log("Elapsed:", event.elapsedTime);
},
});
// Pause/resume frame task
handle.pause();
handle.resume();
// Remove task
handle.remove();
// One-time frame callback
scheduling.schedule(() => {
console.log("Executed on next frame");
});
一般的なパターン
クリーンアップパターン
class MyComponent {
constructor(view) {
this.view = view;
this.handles = new Handles();
this.setup();
}
setup() {
this.handles.add([
reactiveUtils.watch(
() => this.view.scale,
(scale) => this.onScaleChange(scale),
),
reactiveUtils.watch(
() => this.view.extent,
(extent) => this.onExtentChange(extent),
),
this.view.on("click", (e) => this.onClick(e)),
]);
}
destroy() {
this.handles.removeAll();
}
}
デバウンスを使用した更新
const debouncedUpdate = promiseUtils.debounce(async () => {
const extent = view.extent;
const results = await layer.queryFeatures({
geometry: extent,
returnGeometry: true,
});
updateDisplay(results);
});
reactiveUtils.watch(() => view.stationary && view.extent, debouncedUpdate);
条件付き監視
// Only react when view is stationary
reactiveUtils.watch(
() => (view.stationary ? view.extent : null),
(extent) => {
if (extent) {
updateForExtent(extent);
}
},
);
国際化(intl)
intl モジュールは、数値、日付、座標のロケール対応フォーマットを提供します。
数値のフォーマット
import * as intl from "@arcgis/core/intl.js";
// Format numbers with locale settings
const formatted = intl.formatNumber(1234567.89);
// "1,234,567.89" (en-US) or "1.234.567,89" (de-DE)
// Format with options
const currency = intl.formatNumber(1234.5, {
style: "currency",
currency: "USD",
});
// "$1,234.50"
const percent = intl.formatNumber(0.75, {
style: "percent",
});
// "75%"
日付のフォーマット
import * as intl from "@arcgis/core/intl.js";
const date = new Date();
// Format dates
const formatted = intl.formatDate(date);
// Format with options
const longDate = intl.formatDate(date, {
dateStyle: "full",
timeStyle: "short",
});
// Format date only (from ISO string)
const dateOnly = intl.formatDateOnly("2024-03-15");
// Format time only (from ISO string)
const timeOnly = intl.formatTimeOnly("14:30:00");
// Format timestamp (from ISO string)
const timestamp = intl.formatTimestamp("2024-03-15T14:30:00Z");
ロケールの設定
import * as intl from "@arcgis/core/intl.js";
// Get current locale
const locale = intl.getLocale();
// Get language code
const lang = intl.getLocaleLanguage();
// Normalize locale string
const normalized = intl.normalizeMessageBundleLocale("en-us"); // "en-US"
メッセージバンドル(i18n)
import * as intl from "@arcgis/core/intl.js";
// Create a JSON message bundle loader
const loader = intl.createJSONLoader({
pattern: "my-app/",
base: "./nls",
});
// Fetch a message bundle
const messages = await intl.fetchMessageBundle("my-app/messages");
console.log(messages.greeting); // Localized string
フォーマット変換ユーティリティ
import * as intl from "@arcgis/core/intl.js";
// Convert SDK date format to Intl options
const intlOptions = intl.convertDateFormatToIntlOptions(
"short-date-short-time",
);
// Convert field format to Intl options
const numberOptions = intl.convertNumberFieldFormatToIntlOptions(fieldFormat);
const dateOptions =
intl.convertDateTimeFieldFormatToIntlOptions(dateFieldFormat);
座標のフォーマット
import * as coordinateFormatter from "@arcgis/core/geometry/coordinateFormatter.js";
// Load the coordinate formatter module
await coordinateFormatter.load();
// Format point as DMS (degrees, minutes, seconds)
const dms = coordinateFormatter.toLatitudeLongitude(point, "dms", 2);
// "34°01'12.00\"N 118°48'18.00\"W"
// Format as UTM
const utm = coordinateFormatter.toUtm(point, "north-south-indicators", true);
// "11S 357811 3764417"
// Parse coordinate string to Point
const parsed = coordinateFormatter.fromLatitudeLongitude("34.02N 118.805W");
参考サンプル
watch-for-changes- マップオブジェクトのプロパティ変更の監視watch-for-changes-reactiveutils- 最新のリアクティブなプロパティ監視chaining-promises- API での Promise チェーンのパターンevent-explorer- ビューとレイヤーのイベントの調査
関連スキル
- reactiveUtils を使用したマップとビューのセットアップについては、
arcgis-core-mapsを参照してください。 - Collection を使用したレイヤー管理については、
arcgis-layersを参照してください。 - イベント処理のパターンについては、
arcgis-interactionを参照してください。
よくある落とし穴
-
メモリリーク:使用後は必ずハンドルを削除してください。
// Anti-pattern: never removing the handle reactiveUtils.watch( () => view.scale, (scale) => updateUI(scale), ); // Handle is lost, watcher runs forever// Correct: store and remove the handle const handle = reactiveUtils.watch( () => view.scale, (scale) => updateUI(scale), ); // Remove when no longer needed handle.remove();影響: ウォッチャーが時間とともに蓄積し、パフォーマンスが低下します。また、破棄されたオブジェクトを参照している場合、エラーが発生する可能性があります。
-
初期値:現在の値をすぐに取得するには、
initial: trueを使用してください。// Anti-pattern: missing initial value reactiveUtils.watch( () => view.scale, (scale) => updateUI(scale), ); // updateUI is NOT called until scale changes// Correct: use initial option reactiveUtils.watch( () => view.scale, (scale) => updateUI(scale), { initial: true }, // Called immediately with current scale );影響: UIが現在の値で初期化されず、最初のプロパティ変更が発生するまで空のまま、または古い状態のままになります。
-
同期と非同期:デフォルトは非同期バッチ処理です。
sync: trueは慎重に使用してください。// Async (default) - batched, better performance reactiveUtils.watch(() => value, callback); // Sync - immediate, can cause performance issues reactiveUtils.watch(() => value, callback, { sync: true });影響: 同期ウォッチャーは変更のたびに実行されます。監視対象のプロパティが頻繁に更新される場合、レイアウトの再計算が繰り返され、パフォーマンスが低下する可能性があります。
-
中断エラー:catchブロックでは必ず中断エラーを確認してください。
// Anti-pattern: treating abort errors as real errors try { await debouncedFunction(); } catch (e) { console.error("Error!", e); // Logs abort errors as errors }// Correct: filter out abort errors try { await debouncedFunction(); } catch (e) { if (!promiseUtils.isAbortError(e)) { console.error("Error!", e); } }影響: 中断エラー(デバウンス処理、キャンセルされたクエリ、ナビゲーションの変更によるもの)が実際のエラーとして記録され、コンソールに大量に出力されるほか、エラー報告システムが作動する可能性があります。