mirror of
https://github.com/maputnik/editor.git
synced 2026-07-25 07:17:26 +00:00
9c1499b805
## Launch Checklist See title, Also removed some "_" from some file names. This is a pure refactoring, no logic changes. - [x] Briefly describe the changes in this PR. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
347 lines
12 KiB
TypeScript
347 lines
12 KiB
TypeScript
import React from "react";
|
|
import {MdAddCircleOutline, MdDelete} from "react-icons/md";
|
|
import latest from "@maplibre/maplibre-gl-style-spec/dist/latest.json";
|
|
import type {GeoJSONSourceSpecification, RasterDEMSourceSpecification, RasterSourceSpecification, SourceSpecification, VectorSourceSpecification} from "maplibre-gl";
|
|
import { type WithTranslation, withTranslation } from "react-i18next";
|
|
|
|
import { Modal } from "./Modal";
|
|
import { InputButton } from "../InputButton";
|
|
import { FieldString } from "../FieldString";
|
|
import { FieldSelect } from "../FieldSelect";
|
|
import { ModalSourcesTypeEditor, type EditorMode } from "./ModalSourcesTypeEditor";
|
|
|
|
import { generateId } from "../../libs/style";
|
|
import { deleteSource, addSource, changeSource } from "../../libs/source";
|
|
import publicSources from "../../config/tilesets.json";
|
|
import { type OnStyleChangedCallback, type StyleSpecificationWithId } from "../../libs/definitions";
|
|
|
|
|
|
type PublicSourceProps = {
|
|
id: string
|
|
type: string
|
|
title: string
|
|
onSelect(...args: unknown[]): unknown
|
|
};
|
|
|
|
class PublicSource extends React.Component<PublicSourceProps> {
|
|
render() {
|
|
return <div className="maputnik-public-source">
|
|
<InputButton
|
|
className="maputnik-public-source-select"
|
|
onClick={() => this.props.onSelect(this.props.id)}
|
|
>
|
|
<div className="maputnik-public-source-info">
|
|
<p className="maputnik-public-source-name">{this.props.title}</p>
|
|
<p className="maputnik-public-source-id">#{this.props.id}</p>
|
|
</div>
|
|
<span className="maputnik-space" />
|
|
<MdAddCircleOutline />
|
|
</InputButton>
|
|
</div>;
|
|
}
|
|
}
|
|
|
|
function editorMode(source: SourceSpecification) {
|
|
if(source.type === "raster") {
|
|
if(source.tiles) return "tile_raster";
|
|
return "tilejson_raster";
|
|
}
|
|
if(source.type === "raster-dem") {
|
|
if(source.tiles) return "tilexyz_raster-dem";
|
|
return "tilejson_raster-dem";
|
|
}
|
|
if(source.type === "vector") {
|
|
if(source.tiles) return "tile_vector";
|
|
if(source.url && source.url.startsWith("pmtiles://")) return "pmtiles_vector";
|
|
return "tilejson_vector";
|
|
}
|
|
if(source.type === "geojson") {
|
|
if (typeof(source.data) === "string") {
|
|
return "geojson_url";
|
|
}
|
|
else {
|
|
return "geojson_json";
|
|
}
|
|
}
|
|
if(source.type === "image") {
|
|
return "image";
|
|
}
|
|
if(source.type === "video") {
|
|
return "video";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
type ActiveModalSourcesTypeEditorProps = {
|
|
sourceId: string
|
|
source: SourceSpecification
|
|
onDelete(...args: unknown[]): unknown
|
|
onChange(...args: unknown[]): unknown
|
|
} & WithTranslation;
|
|
|
|
class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTypeEditorProps> {
|
|
render() {
|
|
const t = this.props.t;
|
|
return <div className="maputnik-active-source-type-editor">
|
|
<div className="maputnik-active-source-type-editor-header">
|
|
<span className="maputnik-active-source-type-editor-header-id">#{this.props.sourceId}</span>
|
|
<span className="maputnik-space" />
|
|
<InputButton
|
|
aria-label={t("Remove '{{sourceId}}' source", {sourceId: this.props.sourceId})}
|
|
className="maputnik-active-source-type-editor-header-delete"
|
|
onClick={()=> this.props.onDelete(this.props.sourceId)}
|
|
style={{backgroundColor: "transparent"}}
|
|
>
|
|
<MdDelete />
|
|
</InputButton>
|
|
</div>
|
|
<div className="maputnik-active-source-type-editor-content">
|
|
<ModalSourcesTypeEditor
|
|
onChange={this.props.onChange}
|
|
mode={editorMode(this.props.source)}
|
|
source={this.props.source}
|
|
/>
|
|
</div>
|
|
</div>;
|
|
}
|
|
}
|
|
|
|
type AddSourceProps = {
|
|
onAdd(...args: unknown[]): unknown
|
|
} & WithTranslation;
|
|
|
|
type AddSourceState = {
|
|
mode: EditorMode
|
|
sourceId: string
|
|
source: SourceSpecification
|
|
};
|
|
|
|
class AddSource extends React.Component<AddSourceProps, AddSourceState> {
|
|
constructor(props: AddSourceProps) {
|
|
super(props);
|
|
this.state = {
|
|
mode: "tilejson_vector",
|
|
sourceId: generateId(),
|
|
source: this.defaultSource("tilejson_vector"),
|
|
};
|
|
}
|
|
|
|
defaultSource(mode: EditorMode): SourceSpecification {
|
|
const source = (this.state || {}).source || {};
|
|
const {protocol} = window.location;
|
|
|
|
switch(mode) {
|
|
case "pmtiles_vector": return {
|
|
type: "vector",
|
|
url: `${protocol}//localhost:3000/file.pmtiles`
|
|
};
|
|
case "geojson_url": return {
|
|
type: "geojson",
|
|
data: `${protocol}//localhost:3000/geojson.json`
|
|
};
|
|
case "geojson_json": return {
|
|
type: "geojson",
|
|
cluster: (source as GeoJSONSourceSpecification).cluster || false,
|
|
data: ""
|
|
};
|
|
case "tilejson_vector": return {
|
|
type: "vector",
|
|
url: (source as VectorSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
|
|
};
|
|
case "tile_vector": return {
|
|
type: "vector",
|
|
tiles: (source as VectorSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.pbf`],
|
|
minzoom: (source as VectorSourceSpecification).minzoom || 0,
|
|
maxzoom: (source as VectorSourceSpecification).maxzoom || 14,
|
|
scheme: (source as VectorSourceSpecification).scheme || "xyz"
|
|
};
|
|
case "tilejson_raster": return {
|
|
type: "raster",
|
|
url: (source as RasterSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
|
|
};
|
|
case "tile_raster": return {
|
|
type: "raster",
|
|
tiles: (source as RasterSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.png`],
|
|
minzoom: (source as RasterSourceSpecification).minzoom || 0,
|
|
maxzoom: (source as RasterSourceSpecification).maxzoom || 14,
|
|
scheme: (source as RasterSourceSpecification).scheme || "xyz",
|
|
tileSize: (source as RasterSourceSpecification).tileSize || 512,
|
|
};
|
|
case "tilejson_raster-dem": return {
|
|
type: "raster-dem",
|
|
url: (source as RasterDEMSourceSpecification).url || `${protocol}//localhost:3000/tilejson.json`
|
|
};
|
|
case "tilexyz_raster-dem": return {
|
|
type: "raster-dem",
|
|
tiles: (source as RasterDEMSourceSpecification).tiles || [`${protocol}//localhost:3000/{x}/{y}/{z}.png`],
|
|
minzoom: (source as RasterDEMSourceSpecification).minzoom || 0,
|
|
maxzoom: (source as RasterDEMSourceSpecification).maxzoom || 14,
|
|
tileSize: (source as RasterDEMSourceSpecification).tileSize || 512
|
|
};
|
|
case "image": return {
|
|
type: "image",
|
|
url: `${protocol}//localhost:3000/image.png`,
|
|
coordinates: [
|
|
[0,0],
|
|
[0,0],
|
|
[0,0],
|
|
[0,0],
|
|
],
|
|
};
|
|
case "video": return {
|
|
type: "video",
|
|
urls: [
|
|
`${protocol}//localhost:3000/movie.mp4`
|
|
],
|
|
coordinates: [
|
|
[0,0],
|
|
[0,0],
|
|
[0,0],
|
|
[0,0],
|
|
],
|
|
};
|
|
default: return {} as any;
|
|
}
|
|
}
|
|
|
|
onAdd = () => {
|
|
const {source, sourceId} = this.state;
|
|
this.props.onAdd(sourceId, source);
|
|
};
|
|
|
|
onChangeSource = (source: SourceSpecification) => {
|
|
this.setState({source});
|
|
};
|
|
|
|
render() {
|
|
const t = this.props.t;
|
|
// Kind of a hack because the type changes, however maputnik has 1..n
|
|
// options per type, for example
|
|
//
|
|
// - 'geojson' - 'GeoJSON (URL)' and 'GeoJSON (JSON)'
|
|
// - 'raster' - 'Raster (TileJSON URL)' and 'Raster (XYZ URL)'
|
|
//
|
|
// So we just ignore the values entirely as they are self explanatory
|
|
const sourceTypeFieldSpec = {
|
|
doc: latest.source_vector.type.doc
|
|
};
|
|
|
|
return <div className="maputnik-add-source">
|
|
<FieldString
|
|
label={t("Source ID")}
|
|
fieldSpec={{doc: t("Unique ID that identifies the source and is used in the layer to reference the source.")}}
|
|
value={this.state.sourceId}
|
|
onChange={(v: string) => this.setState({ sourceId: v})}
|
|
data-wd-key="modal:sources.add.source_id"
|
|
/>
|
|
<FieldSelect
|
|
label={t("Source Type")}
|
|
fieldSpec={sourceTypeFieldSpec}
|
|
options={[
|
|
["geojson_json", t("GeoJSON (JSON)")],
|
|
["geojson_url", t("GeoJSON (URL)")],
|
|
["tilejson_vector", t("Vector (TileJSON URL)")],
|
|
["tile_vector", t("Vector (Tile URLs)")],
|
|
["tilejson_raster", t("Raster (TileJSON URL)")],
|
|
["tile_raster", t("Raster (Tile URLs)")],
|
|
["tilejson_raster-dem", t("Raster DEM (TileJSON URL)")],
|
|
["tilexyz_raster-dem", t("Raster DEM (XYZ URLs)")],
|
|
["pmtiles_vector", t("Vector (PMTiles)")],
|
|
["image", t("Image")],
|
|
["video", t("Video")],
|
|
]}
|
|
onChange={mode => this.setState({mode: mode as EditorMode, source: this.defaultSource(mode as EditorMode)})}
|
|
value={this.state.mode as string}
|
|
data-wd-key="modal:sources.add.source_type"
|
|
/>
|
|
<ModalSourcesTypeEditor
|
|
onChange={this.onChangeSource}
|
|
mode={this.state.mode}
|
|
source={this.state.source}
|
|
/>
|
|
<InputButton
|
|
className="maputnik-add-source-button"
|
|
onClick={this.onAdd}
|
|
data-wd-key="modal:sources.add.add_source"
|
|
>
|
|
{t("Add Source")}
|
|
</InputButton>
|
|
</div>;
|
|
}
|
|
}
|
|
|
|
type ModalSourcesInternalProps = {
|
|
mapStyle: StyleSpecificationWithId
|
|
isOpen: boolean
|
|
onOpenToggle(): void
|
|
onStyleChanged: OnStyleChangedCallback
|
|
} & WithTranslation;
|
|
|
|
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
|
|
stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification {
|
|
const strippedSource = {...source};
|
|
delete strippedSource["title"];
|
|
return strippedSource;
|
|
}
|
|
|
|
render() {
|
|
const {t, mapStyle} = this.props;
|
|
const i18nProps = {t, i18n: this.props.i18n, tReady: this.props.tReady};
|
|
const activeSources = Object.keys(mapStyle.sources).map(sourceId => {
|
|
const source = mapStyle.sources[sourceId];
|
|
return <ActiveModalSourcesTypeEditor
|
|
key={sourceId}
|
|
sourceId={sourceId}
|
|
source={source}
|
|
onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
|
|
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
|
|
{...i18nProps}
|
|
/>;
|
|
});
|
|
|
|
const tilesetOptions = Object.keys(publicSources).filter((sourceId: string) => !(sourceId in mapStyle.sources)).map((sourceId: string) => {
|
|
const source = publicSources[sourceId as keyof typeof publicSources] as SourceSpecification & {title: string};
|
|
return <PublicSource
|
|
key={sourceId}
|
|
id={sourceId}
|
|
type={source.type}
|
|
title={source.title}
|
|
onSelect={() => this.props.onStyleChanged(addSource(mapStyle, sourceId, this.stripTitle(source)))}
|
|
/>;
|
|
});
|
|
|
|
return <Modal
|
|
data-wd-key="modal:sources"
|
|
isOpen={this.props.isOpen}
|
|
onOpenToggle={this.props.onOpenToggle}
|
|
title={t("Sources")}
|
|
>
|
|
<section className="maputnik-modal-section">
|
|
<h1>{t("Active Sources")}</h1>
|
|
{activeSources}
|
|
</section>
|
|
|
|
<section className="maputnik-modal-section">
|
|
<h1>{t("Choose Public Source")}</h1>
|
|
<p>
|
|
{t("Add one of the publicly available sources to your style.")}
|
|
</p>
|
|
<div className="maputnik-public-sources" style={{maxWidth: 500}}>
|
|
{tilesetOptions}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="maputnik-modal-section">
|
|
<h1>{t("Add New Source")}</h1>
|
|
<p>{t("Add a new source to your style. You can only choose the source type and id at creation time!")}</p>
|
|
<AddSource
|
|
onAdd={(sourceId: string, source: SourceSpecification) => this.props.onStyleChanged(addSource(mapStyle, sourceId, source))}
|
|
{...i18nProps}
|
|
/>
|
|
</section>
|
|
</Modal>;
|
|
}
|
|
}
|
|
|
|
export const ModalSources = withTranslation()(ModalSourcesInternal);
|