mcpField
A
arcgis-core-utilities

arcgis-core-utilities

github.com/saschabrunnerch開発2362 回インストール29 週間前に更新
リポジトリ

arcgis-core-utilities について

ArcGIS JavaScript SDKのコアユーティリティの利用パターンと例をまとめたスキルです。リアクティブな状態、コレクション、非同期処理、ワーカー、リクエスト、ローカライズを扱うArcGIS開発者向けです。

主な機能

  1. 01Accessorクラスとプロパティのパターン
  2. 02リアクティブな監視とイベント処理
  3. 03変更通知付きCollectionの利用
  4. 04Promiseとキャンセル用ユーティリティ
  5. 05バックグラウンドワーカー実行
  6. 06リクエストとローカライズ

活用シーン

  1. 01ArcGISのビューやレイヤー状態の監視
  2. 02コレクション変更イベントの管理
  3. 03ワーカーでの重い処理の実行
  4. 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 を参照してください。

よくある落とし穴

  1. メモリリーク:使用後は必ずハンドルを削除してください。

    // 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();
    

    影響: ウォッチャーが時間とともに蓄積し、パフォーマンスが低下します。また、破棄されたオブジェクトを参照している場合、エラーが発生する可能性があります。

  2. 初期値:現在の値をすぐに取得するには、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が現在の値で初期化されず、最初のプロパティ変更が発生するまで空のまま、または古い状態のままになります。

  3. 同期と非同期:デフォルトは非同期バッチ処理です。sync: trueは慎重に使用してください。

    // Async (default) - batched, better performance
    reactiveUtils.watch(() => value, callback);
    
    // Sync - immediate, can cause performance issues
    reactiveUtils.watch(() => value, callback, { sync: true });
    

    影響: 同期ウォッチャーは変更のたびに実行されます。監視対象のプロパティが頻繁に更新される場合、レイアウトの再計算が繰り返され、パフォーマンスが低下する可能性があります。

  4. 中断エラー: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);
      }
    }
    

    影響: 中断エラー(デバウンス処理、キャンセルされたクエリ、ナビゲーションの変更によるもの)が実際のエラーとして記録され、コンソールに大量に出力されるほか、エラー報告システムが作動する可能性があります。

コメント