Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Tile stitching method to GeoRawImage for improved image handling capabilities
- GeoJSON output option for landcover classification results
- Result layer type selector for better visualization control
- Graceful error handling for tile image loading failures
- TMS provider now supports both WebMercator (XYZ) and traditional TMS tile schemes

### Fixed
- Mask generation post-processing improvements
- Multipolygon issues in mask generation
- Landcover classification mask to polygon conversion accuracy
- Image loading now continues when some tiles fail instead of throwing error immediately
- Added proper error type `ImageLoadFailed` for when all tiles fail to load
- TMS provider tile coordinate calculation for Cesium compatibility

### Improved
- Overall library performance with removal of heavy OpenCV dependency
Expand Down
62 changes: 61 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ function MyComponent() {
## Features

- **Multiple AI Tasks**: Object detection, segmentation, classification, and more
- **Map Provider Support**: Geobase, Mapbox, ESRI, and Google Maps
- **Map Provider Support**: Geobase, Mapbox, ESRI, TMS, and Google Maps
- **React Integration**: Hooks for easy React integration
- **TypeScript Support**: Full TypeScript definitions
- **Web Worker Support**: Run AI models in background threads
Expand All @@ -158,6 +158,66 @@ function MyComponent() {

For more see the [supported tasks](https://docs.geobase.app/geoai/supported-tasks)

## Map Providers

GeoAI.js supports multiple map tile providers:

### TMS (Tile Map Service)
TMS is a tile-based map specification that uses a bottom-left origin coordinate system. Perfect for custom tile servers and OpenAerialMap.

```javascript
// Option 1: Using URL template with placeholders (recommended)
const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
provider: "tms",
baseUrl: "https://tile.example.com/tiles/{z}/{x}/{y}.png",
apiKey: "your-api-key", // optional
tileSize: 256, // optional, defaults to 256
attribution: "Custom TMS Provider", // optional
});

// Option 2: Using base URL (legacy format, still supported)
const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
provider: "tms",
baseUrl: "https://tile.example.com/tiles",
extension: "png", // optional, defaults to "png"
apiKey: "your-api-key", // optional
tileSize: 256, // optional, defaults to 256
attribution: "Custom TMS Provider", // optional
});
```

### ESRI
ESRI World Imagery - no API key required.

```javascript
const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
provider: "esri",
});
```

### Mapbox
Requires a Mapbox API key.

```javascript
const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
provider: "mapbox",
apiKey: "your-mapbox-api-key",
style: "mapbox://styles/mapbox/satellite-v9",
});
```

### Geobase
Requires Geobase project credentials.

```javascript
const pipeline = await geoai.pipeline([{ task: "object-detection" }], {
provider: "geobase",
projectRef: "your-project-ref",
apikey: "your-api-key",
cogImagery: "https://path-to-your-cog.tif",
});
```

## Links

- **Documentation**: [docs.geobase.app/geoai](https://docs.geobase.app/geoai) - Comprehensive documentation, examples, and API reference
Expand Down
127 changes: 127 additions & 0 deletions docs/pages/map-providers/tms.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# TMS (Tile Map Service) Provider

The TMS provider allows you to use custom tile services with GeoAI.js. It supports both Web Mercator (XYZ) and traditional TMS tile schemes.

## Tile Schemes

### WebMercator (Default)

The Web Mercator scheme (also known as XYZ or Google Maps scheme) uses:
- **Top-left origin**: Coordinates start from the top-left corner
- **Y axis**: Increases downward
- **Default for**: Most modern tile services including Cesium, Mapbox, Google Maps

```typescript
import { Tms } from "geoai";

const provider = new Tms({
baseUrl: "https://example.com/tiles/{z}/{x}/{y}.png",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scheme: "WebMercator", // This is the default
});
```

### TMS (Traditional)

The traditional TMS scheme uses:
- **Bottom-left origin**: Coordinates start from the bottom-left corner
- **Y axis**: Increases upward
- **Default for**: Traditional TMS services

```typescript
import { Tms } from "geoai";

const provider = new Tms({
baseUrl: "https://example.com/tms/{z}/{x}/{y}.png",
scheme: "TMS", // Use this for traditional TMS services
});
```

## Usage with Cesium

When using with Cesium's `UrlTemplateImageryProvider`, use the `WebMercator` scheme (default):

```typescript
import { Tms } from "geoai";
import { UrlTemplateImageryProvider } from "cesium";

// Create the TMS provider for GeoAI.js
const geoaiProvider = new Tms({
baseUrl: "https://example.com/tiles/{z}/{x}/{y}.png",
scheme: "WebMercator", // Match Cesium's default
});

// Create the Cesium imagery provider
const cesiumProvider = new UrlTemplateImageryProvider({
url: "https://example.com/tiles/{z}/{x}/{y}.png",
// Cesium uses WebMercator by default
});
```

## Configuration Options

```typescript
interface TmsConfig {
baseUrl: string; // Tile URL template
extension?: string; // File extension (default: "png")
apiKey?: string; // API key (added as query parameter)
attribution?: string; // Attribution text (default: "TMS Provider")
tileSize?: number; // Tile size in pixels (default: 256)
headers?: Record<string, string>; // Custom headers
scheme?: "WebMercator" | "TMS"; // Tile scheme (default: "WebMercator")
}
```

## Examples

### URL Template with Placeholders

```typescript
const provider = new Tms({
baseUrl: "https://tiles.example.com/{z}/{x}/{y}.png",
attribution: "© Example Tiles",
});
```

### Traditional Path Construction

```typescript
const provider = new Tms({
baseUrl: "https://tiles.example.com",
extension: "jpg",
attribution: "© Example Tiles",
});
// Generates: https://tiles.example.com/{z}/{x}/{y}.jpg
```

### With API Key

```typescript
const provider = new Tms({
baseUrl: "https://tiles.example.com/{z}/{x}/{y}.png",
apiKey: "your-api-key-here",
});
// Generates: https://tiles.example.com/{z}/{x}/{y}.png?apikey=your-api-key-here
```

### TMS Scheme Example

```typescript
const provider = new Tms({
baseUrl: "https://tms.example.com/{z}/{x}/{y}.png",
scheme: "TMS", // Use traditional TMS coordinates
attribution: "© TMS Provider",
});
```

## Troubleshooting

### Tiles appear flipped or in wrong positions

If your tiles appear in the wrong position, you may need to switch the tile scheme:

- If using with Cesium, Mapbox, or most modern services: use `"WebMercator"` (default)
- If using a traditional TMS service: use `"TMS"`

### Getting incorrect tile coordinates

The GeoAI.js library uses the Web Mercator scheme by default. Make sure your `scheme` configuration matches your tile service's coordinate system.
13 changes: 12 additions & 1 deletion src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ export type EsriParams = {
attribution?: string;
};

export type TmsParams = {
provider: "tms";
baseUrl: string;
extension?: string;
apiKey?: string;
attribution?: string;
tileSize?: number;
headers?: Record<string, string>;
};

export interface InferenceInputs {
polygon: GeoJSON.Feature;
classLabel?: string;
Expand Down Expand Up @@ -81,7 +91,8 @@ export type ProviderParams =
| MapboxParams
| SentinelParams
| GeobaseParams
| EsriParams;
| EsriParams
| TmsParams;

export type HuggingFaceModelTask =
| "mask-generation"
Expand Down
77 changes: 59 additions & 18 deletions src/data_providers/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,47 @@ export const getImageFromTiles = async (
const tileUrlsGrid = tilesGrid.map((row: any) =>
row.map((tile: any) => tile.tileUrl)
);
// Load all images in parallel
const tileImages: RawImage[][] = await Promise.all(
// Load all images in parallel with error handling
const tileImages: (RawImage | null)[][] = await Promise.all(
tileUrlsGrid.map((row: any) =>
Promise.all(row.map(async (url: string) => await load_image(url)))
Promise.all(
row.map(async (url: string) => {
try {
return await load_image(url);
} catch (error) {
console.warn(`Failed to load image from ${url}:`, error);
return null;
}
})
)
)
);

// Check if any images failed to load
const failedTiles: string[] = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to loop twice over the tiles array - the failed list can be created in the catch above

tileImages.forEach((row, rowIndex) => {
row.forEach((image, colIndex) => {
if (image === null) {
failedTiles.push(tileUrlsGrid[rowIndex][colIndex]);
}
});
});

// If all tiles failed, throw an error
if (failedTiles.length === tileImages.flat().length) {
throw new GeobaseError(
ErrorType.ImageLoadFailed,
`Failed to load all tiles. Please check your network connection and tile URLs.`
);
}

// If some tiles failed, log a warning but continue
if (failedTiles.length > 0) {
console.warn(
`Failed to load ${failedTiles.length} out of ${tileImages.flat().length} tiles. Continuing with available tiles.`
);
}

const cornerTiles = [
tilesGrid[0][0], // Top-left
tilesGrid[0][tilesGrid[0].length - 1], // Top-right
Expand All @@ -131,25 +166,31 @@ export const getImageFromTiles = async (
west: Math.min(...cornerTiles.map((tile: any) => tile.tileGeoJson.bbox[0])),
};
if (stitch) {
return GeoRawImage.fromPatches(tileImages, bounds, "EPSG:4326");
// Filter out null values before passing to fromPatches
const validTileImages: RawImage[][] = tileImages.map(row =>
row.filter((img): img is RawImage => img !== null)
);
return GeoRawImage.fromPatches(validTileImages, bounds, "EPSG:4326");
}

// If not stitching, set bounds for each individual GeoRawImage
const geoRawImages: GeoRawImage[][] = tilesGrid.map(
(row: any, rowIndex: number) =>
row.map((tile: any, colIndex: number) => {
const tileBounds = {
north: tile.tileGeoJson.bbox[1],
south: tile.tileGeoJson.bbox[3],
east: tile.tileGeoJson.bbox[2],
west: tile.tileGeoJson.bbox[0],
};
return GeoRawImage.fromRawImage(
tileImages[rowIndex][colIndex],
tileBounds,
"EPSG:4326"
);
})
(row: { tileGeoJson: { bbox: number[] } }[], rowIndex: number) =>
row
.map((tile, colIndex: number) => {
const image = tileImages[rowIndex][colIndex];
if (image === null) {
return null;
}
const tileBounds = {
north: tile.tileGeoJson.bbox[1],
south: tile.tileGeoJson.bbox[3],
east: tile.tileGeoJson.bbox[2],
west: tile.tileGeoJson.bbox[0],
};
return GeoRawImage.fromRawImage(image, tileBounds, "EPSG:4326");
})
.filter((img): img is GeoRawImage => img !== null)
);

return geoRawImages;
Expand Down
Loading
Loading