Merge branch 'main' into main

This commit is contained in:
Harel M
2024-08-21 08:38:17 +03:00
committed by GitHub
56 changed files with 2526 additions and 501 deletions

35
cypress/e2e/i18n.cy.ts Normal file
View File

@@ -0,0 +1,35 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("i18n", () => {
let { beforeAndAfter, get, when, then } = new MaputnikDriver();
beforeAndAfter();
describe("language detector", () => {
it("English", () => {
const url = "?lng=en";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("en");
});
it("Japanese", () => {
const url = "?lng=ja";
when.visit(url);
then(get.elementByTestId("maputnik-lang-select")).shouldHaveValue("ja");
});
});
describe("language switcher", () => {
beforeEach(() => {
when.setStyle("layer");
});
it("the language switcher switches to Japanese", () => {
const selector = "maputnik-lang-select";
then(get.elementByTestId(selector)).shouldExist();
when.select(selector, "ja");
then(get.elementByTestId(selector)).shouldHaveValue("ja");
then(get.elementByTestId("nav:settings")).shouldHaveText("スタイル設定");
});
});
});

View File

@@ -131,7 +131,9 @@ export class MaputnikDriver {
this.helper.when.acceptConfirm();
}
// when methods should not include assertions
this.helper.get.elementByTestId("toolbar:link").should("be.visible");
const toolbarLink = this.helper.get.elementByTestId("toolbar:link")
toolbarLink.scrollIntoView();
toolbarLink.should("be.visible");
},
typeKeys: (keys: string) => this.helper.get.element("body").type(keys),

17
i18next-parser.config.ts Normal file
View File

@@ -0,0 +1,17 @@
export default {
output: 'src/locales/$LOCALE/$NAMESPACE.json',
locales: [ 'ja', 'he','zh' ],
// Because some keys are dynamically generated, i18next-parser can't detect them.
// We add these keys manually, so we don't want to remove them.
keepRemoved: true,
// We use plain English keys, so we disable key and namespace separators.
keySeparator: false,
namespaceSeparator: false,
defaultValue: (locale, ns, key) => {
// The default value is a string that indicates that the string is not translated.
return '__STRING_NOT_TRANSLATED__';
}
}

1186
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@
"scripts": {
"start": "vite",
"build": "tsc && vite build --base=/maputnik/",
"i18n:refresh": "i18next 'src/**/*.{ts,tsx,js,jsx}'",
"lint": "eslint ./src ./cypress --ext ts,tsx,js,jsx --report-unused-disable-directives --max-warnings 0",
"test": "cypress run",
"cy:open": "cypress open",
@@ -38,6 +39,9 @@
"detect-browser": "^5.3.0",
"events": "^3.3.0",
"file-saver": "^2.0.5",
"i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-resources-to-backend": "^1.2.1",
"json-stringify-pretty-compact": "^4.0.0",
"json-to-ast": "^2.1.0",
"jsonlint": "github:josdejong/jsonlint#85a19d7",
@@ -63,6 +67,7 @@
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-file-reader-input": "^2.0.0",
"react-i18next": "^15.0.1",
"react-icon-base": "^2.1.2",
"react-icons": "^5.0.1",
"react-sortable-hoc": "^2.0.0",
@@ -121,11 +126,12 @@
"@types/uuid": "^9.0.8",
"@vitejs/plugin-react": "^4.2.1",
"cors": "^2.8.5",
"cypress": "^13.10.0",
"cypress": "^13.13.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"i18next-parser": "^9.0.1",
"istanbul": "^0.4.5",
"istanbul-lib-coverage": "^3.2.2",
"mocha": "^10.3.0",

View File

@@ -24,7 +24,6 @@ import ModalExport from './ModalExport'
import ModalSources from './ModalSources'
import ModalOpen from './ModalOpen'
import ModalShortcuts from './ModalShortcuts'
import ModalSurvey from './ModalSurvey'
import ModalDebug from './ModalDebug'
import {downloadGlyphsMetadata, downloadSpriteMetadata} from '../libs/metadata'
@@ -128,7 +127,6 @@ type AppState = {
open: boolean
shortcuts: boolean
export: boolean
survey: boolean
debug: boolean
}
}
@@ -137,7 +135,6 @@ export default class App extends React.Component<any, AppState> {
revisionStore: RevisionStore;
styleStore: StyleStore | ApiStyleStore;
layerWatcher: LayerWatcher;
shortcutEl: ModalShortcuts | null = null;
constructor(props: any) {
super(props)
@@ -277,7 +274,6 @@ export default class App extends React.Component<any, AppState> {
shortcuts: false,
export: false,
// TODO: Disabled for now, this should be opened on the Nth visit to the editor
survey: false,
debug: false,
},
maplibreGlDebugOptions: {
@@ -839,10 +835,6 @@ export default class App extends React.Component<any, AppState> {
}
setModal(modalName: keyof AppState["isOpen"], value: boolean) {
if(modalName === 'survey' && value === false) {
localStorage.setItem('survey', '');
}
this.setState({
isOpen: {
...this.state.isOpen,
@@ -942,7 +934,6 @@ export default class App extends React.Component<any, AppState> {
mapView={this.state.mapView}
/>
<ModalShortcuts
ref={(el) => this.shortcutEl = el}
isOpen={this.state.isOpen.shortcuts}
onOpenToggle={this.toggleModal.bind(this, 'shortcuts')}
/>
@@ -970,10 +961,6 @@ export default class App extends React.Component<any, AppState> {
isOpen={this.state.isOpen.sources}
onOpenToggle={this.toggleModal.bind(this, 'sources')}
/>
<ModalSurvey
isOpen={this.state.isOpen.survey}
onOpenToggle={this.toggleModal.bind(this, 'survey')}
/>
</div>
return <AppLayout

View File

@@ -1,17 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import ScrollContainer from './ScrollContainer'
import { WithTranslation, withTranslation } from 'react-i18next';
type AppLayoutProps = {
type AppLayoutInternalProps = {
toolbar: React.ReactElement
layerList: React.ReactElement
layerEditor?: React.ReactElement
map: React.ReactElement
bottom?: React.ReactElement
modals?: React.ReactNode
};
} & WithTranslation;
class AppLayout extends React.Component<AppLayoutProps> {
class AppLayoutInternal extends React.Component<AppLayoutInternalProps> {
static childContextTypes = {
reactIconBase: PropTypes.object
}
@@ -23,17 +24,21 @@ class AppLayout extends React.Component<AppLayoutProps> {
}
render() {
document.body.dir = this.props.i18n.dir();
return <div className="maputnik-layout">
{this.props.toolbar}
<div className="maputnik-layout-list">
{this.props.layerList}
<div className="maputnik-layout-main">
<div className="maputnik-layout-list">
{this.props.layerList}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
</div>
<div className="maputnik-layout-drawer">
<ScrollContainer>
{this.props.layerEditor}
</ScrollContainer>
</div>
{this.props.map}
{this.props.bottom && <div className="maputnik-layout-bottom">
{this.props.bottom}
</div>
@@ -43,4 +48,5 @@ class AppLayout extends React.Component<AppLayoutProps> {
}
}
export default AppLayout
const AppLayout = withTranslation()(AppLayoutInternal);
export default AppLayout;

View File

@@ -1,23 +1,24 @@
import React from 'react'
import {formatLayerId} from '../libs/format';
import {LayerSpecification, StyleSpecification} from 'maplibre-gl';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
type AppMessagePanelProps = {
type AppMessagePanelInternalProps = {
errors?: unknown[]
infos?: string[]
mapStyle?: StyleSpecification
onLayerSelect?(...args: unknown[]): unknown
currentLayer?: LayerSpecification
selectedLayerIndex?: number
};
} & WithTranslation;
export default class AppMessagePanel extends React.Component<AppMessagePanelProps> {
class AppMessagePanelInternal extends React.Component<AppMessagePanelInternalProps> {
static defaultProps = {
onLayerSelect: () => {},
}
render() {
const {selectedLayerIndex} = this.props;
const {t, selectedLayerIndex} = this.props;
const errors = this.props.errors?.map((error: any, idx) => {
let content;
if (error.parsed && error.parsed.type === "layer") {
@@ -25,7 +26,9 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
const layerId = this.props.mapStyle?.layers[parsed.data.index].id;
content = (
<>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
<Trans t={t}>
Layer <span>{formatLayerId(layerId)}</span>: {parsed.data.message}
</Trans>
{selectedLayerIndex !== parsed.data.index &&
<>
&nbsp;&mdash;&nbsp;
@@ -33,7 +36,7 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
className="maputnik-message-panel__switch-button"
onClick={() => this.props.onLayerSelect!(parsed.data.index)}
>
switch to layer
{t("switch to layer")}
</button>
</>
}
@@ -59,3 +62,5 @@ export default class AppMessagePanel extends React.Component<AppMessagePanelProp
}
}
const AppMessagePanel = withTranslation()(AppMessagePanelInternal);
export default AppMessagePanel;

View File

@@ -2,10 +2,12 @@ import React from 'react'
import classnames from 'classnames'
import {detect} from 'detect-browser';
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage} from 'react-icons/md'
import {MdFileDownload, MdOpenInBrowser, MdSettings, MdLayers, MdHelpOutline, MdFindInPage, MdLanguage} from 'react-icons/md'
import pkgJson from '../../package.json'
//@ts-ignore
import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
import { withTranslation, WithTranslation } from 'react-i18next';
import { supportedLanguages } from '../i18n';
// This is required because of <https://stackoverflow.com/a/49846426>, there isn't another way to detect support that I'm aware of.
const browser = detect();
@@ -80,7 +82,7 @@ class ToolbarAction extends React.Component<ToolbarActionProps> {
export type MapState = "map" | "inspect" | "filter-achromatopsia" | "filter-deuteranopia" | "filter-protanopia" | "filter-tritanopia";
type AppToolbarProps = {
type AppToolbarInternalProps = {
mapStyle: object
inspectModeEnabled: boolean
onStyleChanged(...args: unknown[]): unknown
@@ -93,9 +95,9 @@ type AppToolbarProps = {
onSetMapState(mapState: MapState): unknown
mapState?: MapState
renderer?: string
};
} & WithTranslation;
export default class AppToolbar extends React.Component<AppToolbarProps> {
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
state = {
isOpen: {
settings: false,
@@ -110,6 +112,10 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
this.props.onSetMapState(val);
}
handleLanguageChange(val: string) {
this.props.i18n.changeLanguage(val);
}
onSkip = (target: string) => {
if (target === "map") {
(document.querySelector(".maplibregl-canvas") as HTMLCanvasElement).focus();
@@ -121,40 +127,41 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
}
render() {
const t = this.props.t;
const views = [
{
id: "map",
group: "general",
title: "Map",
title: t("Map"),
},
{
id: "inspect",
group: "general",
title: "Inspect",
title: t("Inspect"),
disabled: this.props.renderer === 'ol',
},
{
id: "filter-deuteranopia",
group: "color-accessibility",
title: "Deuteranopia filter",
title: t("Deuteranopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-protanopia",
group: "color-accessibility",
title: "Protanopia filter",
title: t("Protanopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-tritanopia",
group: "color-accessibility",
title: "Tritanopia filter",
title: t("Tritanopia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
{
id: "filter-achromatopsia",
group: "color-accessibility",
title: "Achromatopsia filter",
title: t("Achromatopsia filter"),
disabled: !colorAccessibilityFiltersEnabled,
},
];
@@ -174,21 +181,21 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-list")}
>
Layers list
{t("Layers list")}
</button>
<button
data-wd-key="root:skip:layer-editor"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("layer-editor")}
>
Layer editor
{t("Layer editor")}
</button>
<button
data-wd-key="root:skip:map-view"
className="maputnik-toolbar-skip"
onClick={_e => this.onSkip("map")}
>
Map view
{t("Map view")}
</button>
<a
className="maputnik-toolbar-logo"
@@ -196,7 +203,7 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
rel="noreferrer noopener"
href="https://github.com/maplibre/maputnik"
>
<img src={maputnikLogo} alt="Maputnik on GitHub" />
<img src={maputnikLogo} alt={t("Maputnik on GitHub")} />
<h1>
<span className="maputnik-toolbar-name">{pkgJson.name}</span>
<span className="maputnik-toolbar-version">v{pkgJson.version}</span>
@@ -206,24 +213,24 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
<div className="maputnik-toolbar__actions" role="navigation" aria-label="Toolbar">
<ToolbarAction wdKey="nav:open" onClick={this.props.onToggleModal.bind(this, 'open')}>
<MdOpenInBrowser />
<IconText>Open</IconText>
<IconText>{t("Open")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:export" onClick={this.props.onToggleModal.bind(this, 'export')}>
<MdFileDownload />
<IconText>Export</IconText>
<IconText>{t("Export")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:sources" onClick={this.props.onToggleModal.bind(this, 'sources')}>
<MdLayers />
<IconText>Data Sources</IconText>
<IconText>{t("Data Sources")}</IconText>
</ToolbarAction>
<ToolbarAction wdKey="nav:settings" onClick={this.props.onToggleModal.bind(this, 'settings')}>
<MdSettings />
<IconText>Style Settings</IconText>
<IconText>{t("Style Settings")}</IconText>
</ToolbarAction>
<ToolbarSelect wdKey="nav:inspect">
<MdFindInPage />
<label>View
<label>{t("View")}
<select
className="maputnik-select"
data-wd-key="maputnik-select"
@@ -237,7 +244,7 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
</option>
);
})}
<optgroup label="Color accessibility">
<optgroup label={t("Color accessibility")}>
{views.filter(v => v.group === "color-accessibility").map((item) => {
return (
<option key={item.id} value={item.id} disabled={item.disabled}>
@@ -250,12 +257,35 @@ export default class AppToolbar extends React.Component<AppToolbarProps> {
</label>
</ToolbarSelect>
<ToolbarSelect wdKey="nav:language">
<MdLanguage />
<label>{t("Language")}
<select
className="maputnik-select"
data-wd-key="maputnik-lang-select"
onChange={(e) => this.handleLanguageChange(e.target.value)}
value={this.props.i18n.language}
>
{Object.entries(supportedLanguages).map(([code, name]) => {
return (
<option key={code} value={code}>
{name}
</option>
);
})}
</select>
</label>
</ToolbarSelect>
<ToolbarLink href={"https://github.com/maplibre/maputnik/wiki"}>
<MdHelpOutline />
<IconText>Help</IconText>
<IconText>{t("Help")}</IconText>
</ToolbarLink>
</div>
</div>
</nav>
}
}
const AppToolbar = withTranslation()(AppToolbarInternal);
export default AppToolbar;

View File

@@ -2,21 +2,23 @@ import React from 'react'
import Block from './Block'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldCommentProps = {
type FieldCommentInternalProps = {
value?: string
onChange(value: string | undefined): unknown
error: {message: string}
};
} & WithTranslation;
export default class FieldComment extends React.Component<FieldCommentProps> {
class FieldCommentInternal extends React.Component<FieldCommentInternalProps> {
render() {
const t = this.props.t;
const fieldSpec = {
doc: "Comments for the current layer. This is non-standard and not in the spec."
doc: t("Comments for the current layer. This is non-standard and not in the spec."),
};
return <Block
label={"Comments"}
label={t("Comments")}
fieldSpec={fieldSpec}
data-wd-key="layer-comment"
error={this.props.error}
@@ -25,9 +27,12 @@ export default class FieldComment extends React.Component<FieldCommentProps> {
multi={true}
value={this.props.value}
onChange={this.props.onChange}
default="Comment..."
default={t("Comment...")}
data-wd-key="layer-comment.input"
/>
</Block>
}
}
const FieldComment = withTranslation()(FieldCommentInternal);
export default FieldComment;

View File

@@ -128,7 +128,7 @@ export default class FieldFunction extends React.Component<FieldFunctionProps, F
}
}
static getDerivedStateFromProps(props: FieldFunctionProps, state: FieldFunctionState) {
static getDerivedStateFromProps(props: Readonly<FieldFunctionProps>, state: FieldFunctionState) {
// Because otherwise when editing values we end up accidentally changing field type.
if (state.isEditing) {
return {};

View File

@@ -13,7 +13,8 @@ type FieldIdProps = {
export default class FieldId extends React.Component<FieldIdProps> {
render() {
return <Block label={"ID"} fieldSpec={latest.layer.id}
return <Block label="ID" fieldSpec={latest.layer.id}
data-wd-key={this.props.wdKey}
error={this.props.error}
>

View File

@@ -3,16 +3,18 @@ import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMaxZoomProps = {
type FieldMaxZoomInternalProps = {
value?: number
onChange(value: number | undefined): unknown
error?: {message: string}
};
} & WithTranslation;
export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
class FieldMaxZoomInternal extends React.Component<FieldMaxZoomInternalProps> {
render() {
return <Block label={"Max Zoom"} fieldSpec={latest.layer.maxzoom}
const t = this.props.t;
return <Block label={t("Max Zoom")} fieldSpec={latest.layer.maxzoom}
error={this.props.error}
data-wd-key="max-zoom"
>
@@ -28,3 +30,6 @@ export default class FieldMaxZoom extends React.Component<FieldMaxZoomProps> {
</Block>
}
}
const FieldMaxZoom = withTranslation()(FieldMaxZoomInternal);
export default FieldMaxZoom;

View File

@@ -3,16 +3,18 @@ import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputNumber from './InputNumber'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldMinZoomProps = {
type FieldMinZoomInternalProps = {
value?: number
onChange(...args: unknown[]): unknown
error?: {message: string}
};
} & WithTranslation;
export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
class FieldMinZoomInternal extends React.Component<FieldMinZoomInternalProps> {
render() {
return <Block label={"Min Zoom"} fieldSpec={latest.layer.minzoom}
const t = this.props.t;
return <Block label={t("Min Zoom")} fieldSpec={latest.layer.minzoom}
error={this.props.error}
data-wd-key="min-zoom"
>
@@ -28,3 +30,6 @@ export default class FieldMinZoom extends React.Component<FieldMinZoomProps> {
</Block>
}
}
const FieldMinZoom = withTranslation()(FieldMinZoomInternal);
export default FieldMinZoom;

View File

@@ -3,24 +3,26 @@ import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceProps = {
type FieldSourceInternalProps = {
value?: string
wdKey?: string
onChange?(value: string| undefined): unknown
sourceIds?: unknown[]
error?: {message: string}
};
} & WithTranslation;
export default class FieldSource extends React.Component<FieldSourceProps> {
class FieldSourceInternal extends React.Component<FieldSourceInternalProps> {
static defaultProps = {
onChange: () => {},
sourceIds: [],
}
render() {
const t = this.props.t;
return <Block
label={"Source"}
label={t("Source")}
fieldSpec={latest.layer.source}
error={this.props.error}
data-wd-key={this.props.wdKey}
@@ -33,3 +35,6 @@ export default class FieldSource extends React.Component<FieldSourceProps> {
</Block>
}
}
const FieldSource = withTranslation()(FieldSourceInternal);
export default FieldSource;

View File

@@ -3,16 +3,17 @@ import React from 'react'
import {latest} from '@maplibre/maplibre-gl-style-spec'
import Block from './Block'
import InputAutocomplete from './InputAutocomplete'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldSourceLayerProps = {
type FieldSourceLayerInternalProps = {
value?: string
onChange?(...args: unknown[]): unknown
sourceLayerIds?: unknown[]
isFixed?: boolean
error?: {message: string}
};
} & WithTranslation;
export default class FieldSourceLayer extends React.Component<FieldSourceLayerProps> {
class FieldSourceLayerInternal extends React.Component<FieldSourceLayerInternalProps> {
static defaultProps = {
onChange: () => {},
sourceLayerIds: [],
@@ -20,8 +21,9 @@ export default class FieldSourceLayer extends React.Component<FieldSourceLayerPr
}
render() {
const t = this.props.t;
return <Block
label={"Source Layer"}
label={t("Source Layer")}
fieldSpec={latest.layer['source-layer']}
data-wd-key="layer-source-layer"
error={this.props.error}
@@ -35,3 +37,6 @@ export default class FieldSourceLayer extends React.Component<FieldSourceLayerPr
</Block>
}
}
const FieldSourceLayer = withTranslation()(FieldSourceLayerInternal);
export default FieldSourceLayer;

View File

@@ -4,22 +4,24 @@ import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import Block from './Block'
import InputSelect from './InputSelect'
import InputString from './InputString'
import { WithTranslation, withTranslation } from 'react-i18next';
type FieldTypeProps = {
type FieldTypeInternalProps = {
value: string
wdKey?: string
onChange(value: string): unknown
error?: {message: string}
disabled?: boolean
};
} & WithTranslation;
export default class FieldType extends React.Component<FieldTypeProps> {
class FieldTypeInternal extends React.Component<FieldTypeInternalProps> {
static defaultProps = {
disabled: false,
}
render() {
return <Block label={"Type"} fieldSpec={latest.layer.type}
const t = this.props.t;
return <Block label={t("Type")} fieldSpec={latest.layer.type}
data-wd-key={this.props.wdKey}
error={this.props.error}
>
@@ -50,3 +52,6 @@ export default class FieldType extends React.Component<FieldTypeProps> {
</Block>
}
}
const FieldType = withTranslation()(FieldTypeInternal);
export default FieldType;

View File

@@ -13,9 +13,10 @@ import FilterEditorBlock from './FilterEditorBlock'
import InputButton from './InputButton'
import Doc from './Doc'
import ExpressionProperty from './_ExpressionProperty';
import { WithTranslation, withTranslation } from 'react-i18next';
function combiningFilter(props: FilterEditorProps): LegacyFilterSpecification | ExpressionSpecification {
function combiningFilter(props: FilterEditorInternalProps): LegacyFilterSpecification | ExpressionSpecification {
const filter = props.filter || ['all'];
if (!Array.isArray(filter)) {
@@ -89,13 +90,13 @@ function hasNestedCombiningFilter(filter: LegacyFilterSpecification | Expression
return false
}
type FilterEditorProps = {
type FilterEditorInternalProps = {
/** Properties of the vector layer and the available fields */
properties?: {[key:string]: any}
filter?: any[]
errors?: {[key:string]: any}
onChange(value: LegacyFilterSpecification | ExpressionSpecification): unknown
};
} & WithTranslation;
type FilterEditorState = {
showDoc: boolean
@@ -103,12 +104,12 @@ type FilterEditorState = {
valueIsSimpleFilter?: boolean
};
export default class FilterEditor extends React.Component<FilterEditorProps, FilterEditorState> {
class FilterEditorInternal extends React.Component<FilterEditorInternalProps, FilterEditorState> {
static defaultProps = {
filter: ["all"],
}
constructor (props: FilterEditorProps) {
constructor (props: FilterEditorInternalProps) {
super(props);
this.state = {
showDoc: false,
@@ -155,17 +156,17 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
})
}
static getDerivedStateFromProps(props: FilterEditorProps, currentState: FilterEditorState) {
static getDerivedStateFromProps(props: Readonly<FilterEditorInternalProps>, state: FilterEditorState) {
const displaySimpleFilter = checkIfSimpleFilter(combiningFilter(props));
// Upgrade but never downgrade
if (!displaySimpleFilter && currentState.displaySimpleFilter === true) {
if (!displaySimpleFilter && state.displaySimpleFilter === true) {
return {
displaySimpleFilter: false,
valueIsSimpleFilter: false,
};
}
else if (displaySimpleFilter && currentState.displaySimpleFilter === false) {
else if (displaySimpleFilter && state.displaySimpleFilter === false) {
return {
valueIsSimpleFilter: true,
}
@@ -178,7 +179,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
}
render() {
const {errors} = this.props;
const {errors, t} = this.props;
const {displaySimpleFilter} = this.state;
const fieldSpec={
doc: latest.layer.filter.doc + " Combine multiple filters together by using a compound filter."
@@ -190,16 +191,16 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
if (isNestedCombiningFilter) {
return <div className="maputnik-filter-editor-unsupported">
<p>
Nested filters are not supported.
{t("Nested filters are not supported.")}
</p>
<InputButton
onClick={this.makeExpression}
title="Convert to expression"
title={t("Convert to expression")}
>
<svg style={{marginRight: "0.2em", width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg>
Upgrade to expression
{t("Upgrade to expression")}
</InputButton>
</div>
}
@@ -212,7 +213,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
<div>
<InputButton
onClick={this.makeExpression}
title="Convert to expression"
title={t("Convert to expression")}
className="maputnik-make-zoom-function"
>
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
@@ -247,13 +248,17 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
<Block
key="top"
fieldSpec={fieldSpec}
label={"Filter"}
label={t("Filter")}
action={actions}
>
<InputSelect
value={combiningOp}
onChange={(v: [string, any]) => this.onFilterPartChanged(0, v)}
options={[["all", "every filter matches"], ["none", "no filter matches"], ["any", "any filter matches"]]}
options={[
["all", t("every filter matches")],
["none", t("no filter matches")],
["any", t("any filter matches")]
]}
/>
</Block>
{editorBlocks}
@@ -268,7 +273,7 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add filter
</svg> {t("Add filter")}
</InputButton>
</div>
<div
@@ -299,12 +304,13 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
/>
{this.state.valueIsSimpleFilter &&
<div className="maputnik-expr-infobox">
You&apos;ve entered a old style filter,{' '}
{t("You've entered an old style filter.")}
{' '}
<button
onClick={this.makeFilter}
className="maputnik-expr-infobox__button"
>
switch to filter editor
{t("Switch to filter editor.")}
</button>
</div>
}
@@ -313,3 +319,6 @@ export default class FilterEditor extends React.Component<FilterEditorProps, Fil
}
}
}
const FilterEditor = withTranslation()(FilterEditorInternal);
export default FilterEditor;

View File

@@ -1,19 +1,21 @@
import React, { PropsWithChildren } from 'react'
import InputButton from './InputButton'
import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
type FilterEditorBlockProps = PropsWithChildren & {
type FilterEditorBlockInternalProps = PropsWithChildren & {
onDelete(...args: unknown[]): unknown
};
} & WithTranslation;
export default class FilterEditorBlock extends React.Component<FilterEditorBlockProps> {
class FilterEditorBlockInternal extends React.Component<FilterEditorBlockInternalProps> {
render() {
const t = this.props.t;
return <div className="maputnik-filter-editor-block">
<div className="maputnik-filter-editor-block-action">
<InputButton
className="maputnik-delete-filter"
onClick={this.props.onDelete}
title="Delete filter block"
title={t("Delete filter block")}
>
<MdDelete />
</InputButton>
@@ -25,3 +27,5 @@ export default class FilterEditorBlock extends React.Component<FilterEditorBlock
}
}
const FilterEditorBlock = withTranslation()(FilterEditorBlockInternal);
export default FilterEditorBlock;

View File

@@ -32,7 +32,7 @@ export default class FieldArray extends React.Component<FieldArrayProps, FieldAr
};
}
static getDerivedStateFromProps(props: FieldArrayProps, state: FieldArrayState) {
static getDerivedStateFromProps(props: Readonly<FieldArrayProps>, state: FieldArrayState) {
const value: any[] = [];
const initialPropsValue = state.initialPropsValue.slice(0);

View File

@@ -1,6 +1,7 @@
import React from 'react'
import capitalize from 'lodash.capitalize'
import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
import InputString from './InputString'
import InputNumber from './InputNumber'
@@ -21,10 +22,11 @@ export type FieldDynamicArrayProps = {
}
'aria-label'?: string
label: string
};
}
type FieldDynamicArrayInternalProps = FieldDynamicArrayProps & WithTranslation;
export default class FieldDynamicArray extends React.Component<FieldDynamicArrayProps> {
class FieldDynamicArrayInternal extends React.Component<FieldDynamicArrayInternalProps> {
changeValue(idx: number, newValue: string | number | undefined) {
const values = this.values.slice(0)
values[idx] = newValue
@@ -62,8 +64,13 @@ export default class FieldDynamicArray extends React.Component<FieldDynamicArray
}
render() {
const t = this.props.t;
const i18nProps = { t, i18n: this.props.i18n, tReady: this.props.tReady };
const inputs = this.values.map((v, i) => {
const deleteValueBtn= <DeleteValueInputButton onClick={this.deleteValue.bind(this, i)} />
const deleteValueBtn= <DeleteValueInputButton
onClick={this.deleteValue.bind(this, i)}
{...i18nProps}
/>;
let input;
if(this.props.type === 'url') {
input = <InputUrl
@@ -117,23 +124,27 @@ export default class FieldDynamicArray extends React.Component<FieldDynamicArray
className="maputnik-array-add-value"
onClick={this.addValue}
>
Add value
{t("Add value")}
</InputButton>
</div>
);
}
}
const FieldDynamicArray = withTranslation()(FieldDynamicArrayInternal);
export default FieldDynamicArray;
type DeleteValueInputButtonProps = {
onClick?(...args: unknown[]): unknown
};
} & WithTranslation;
class DeleteValueInputButton extends React.Component<DeleteValueInputButtonProps> {
render() {
const t = this.props.t;
return <InputButton
className="maputnik-delete-stop"
onClick={this.props.onClick}
title="Remove array item"
title={t("Remove array item")}
>
<FieldDocLabel
label={<MdDelete />}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import classnames from 'classnames';
import CodeMirror, { ModeSpec } from 'codemirror';
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import 'codemirror/mode/javascript/javascript'
import 'codemirror/addon/lint/lint'
@@ -27,6 +28,7 @@ export type InputJsonProps = {
mode?: ModeSpec<any>
lint?: boolean | object
};
type InputJsonInternalProps = InputJsonProps & WithTranslation;
type InputJsonState = {
isEditing: boolean
@@ -34,7 +36,7 @@ type InputJsonState = {
prevValue: string
};
export default class InputJson extends React.Component<InputJsonProps, InputJsonState> {
class InputJsonInternal extends React.Component<InputJsonInternalProps, InputJsonState> {
static defaultProps = {
lineNumbers: true,
lineWrapping: false,
@@ -52,7 +54,7 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
_el: HTMLDivElement | null = null;
_cancelNextChange: boolean = false;
constructor(props: InputJsonProps) {
constructor(props: InputJsonInternalProps) {
super(props);
this._keyEvent = "keyboard";
this.state = {
@@ -156,6 +158,7 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
}
render() {
const t = this.props.t;
const {showMessage} = this.state;
const style = {} as {maxHeight?: number};
if (this.props.maxHeight) {
@@ -164,7 +167,9 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
return <div className="JSONEditor" onPointerDown={this.onPointerDown} aria-hidden="true">
<div className={classnames("JSONEditor__message", {"JSONEditor__message--on": showMessage})}>
Press <kbd>ESC</kbd> to lose focus
<Trans t={t}>
Press <kbd>ESC</kbd> to lose focus
</Trans>
</div>
<div
className={classnames("codemirror-container", this.props.className)}
@@ -174,3 +179,6 @@ export default class InputJson extends React.Component<InputJsonProps, InputJson
</div>
}
}
const InputJson = withTranslation()(InputJsonInternal);
export default InputJson;

View File

@@ -41,7 +41,7 @@ export default class InputNumber extends React.Component<InputNumberProps, Input
}
}
static getDerivedStateFromProps(props: InputNumberProps, state: InputNumberState) {
static getDerivedStateFromProps(props: Readonly<InputNumberProps>, state: InputNumberState) {
if (!state.editing && props.value !== state.value) {
return {
value: props.value,

View File

@@ -33,7 +33,7 @@ export default class InputString extends React.Component<InputStringProps, Input
}
}
static getDerivedStateFromProps(props: InputStringProps, state: InputStringState) {
static getDerivedStateFromProps(props: Readonly<InputStringProps>, state: InputStringState) {
if (!state.editing) {
return {
value: props.value

View File

@@ -1,9 +1,10 @@
import React from 'react'
import InputString from './InputString'
import SmallError from './SmallError'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
function validate(url: string) {
function validate(url: string, t: TFunction): JSX.Element | undefined {
if (url === "") {
return;
}
@@ -22,15 +23,19 @@ function validate(url: string) {
const isSsl = window.location.protocol === "https:";
if (!protocol) {
error = (
<SmallError>
Must provide protocol {
isSsl
? <code>https://</code>
: <><code>http://</code> or <code>https://</code></>
}
</SmallError>
);
if (isSsl) {
error = (
<SmallError>
<Trans t={t}>Must provide protocol: <code>https://</code></Trans>
</SmallError>
);
} else {
error = (
<SmallError>
<Trans t={t}>Must provide protocol: <code>http://</code> or <code>https://</code></Trans>
</SmallError>
);
}
}
else if (
protocol &&
@@ -39,7 +44,9 @@ function validate(url: string) {
) {
error = (
<SmallError>
CORS policy won&apos;t allow fetching resources served over http from https, use a <code>https://</code> domain
<Trans t={t}>
CORS policy won&apos;t allow fetching resources served over http from https, use a <code>https://</code> domain
</Trans>
</SmallError>
);
}
@@ -61,32 +68,34 @@ export type FieldUrlProps = {
className?: string
};
type FieldUrlInternalProps = FieldUrlProps & WithTranslation;
type FieldUrlState = {
error?: React.ReactNode
}
export default class FieldUrl extends React.Component<FieldUrlProps, FieldUrlState> {
class FieldUrlInternal extends React.Component<FieldUrlInternalProps, FieldUrlState> {
static defaultProps = {
onInput: () => {},
}
constructor (props: FieldUrlProps) {
constructor (props: FieldUrlInternalProps) {
super(props);
this.state = {
error: validate(props.value)
error: validate(props.value, props.t),
};
}
onInput = (url: string) => {
this.setState({
error: validate(url)
error: validate(url, this.props.t),
});
if (this.props.onInput) this.props.onInput(url);
}
onChange = (url: string) => {
this.setState({
error: validate(url)
error: validate(url, this.props.t),
});
this.props.onChange(url);
}
@@ -106,3 +115,5 @@ export default class FieldUrl extends React.Component<FieldUrlProps, FieldUrlSta
}
}
const FieldUrl = withTranslation()(FieldUrlInternal);
export default FieldUrl;

View File

@@ -19,31 +19,45 @@ import FieldSourceLayer from './FieldSourceLayer'
import { changeType, changeProperty } from '../libs/layer'
import layout from '../config/layout.json'
import {formatLayerId} from '../libs/format';
import { WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next';
function getLayoutForType(type: LayerSpecification["type"]) {
return layout[type] ? layout[type] : layout.invalid;
function getLayoutForType(type: LayerSpecification["type"], t: TFunction) {
return layout[type] ? {
...layout[type],
groups: layout[type].groups.map(group => {
return {
...group,
id: group.title.replace(/ /g, "_"),
title: t(group.title)
};
}),
} : layout.invalid;
}
function layoutGroups(layerType: LayerSpecification["type"]): {title: string, type: string, fields?: string[]}[] {
function layoutGroups(layerType: LayerSpecification["type"], t: TFunction): {id: string, title: string, type: string, fields?: string[]}[] {
const layerGroup = {
title: 'Layer',
id: 'layer',
title: t('Layer'),
type: 'layer'
}
const filterGroup = {
title: 'Filter',
id: 'filter',
title: t('Filter'),
type: 'filter'
}
const editorGroup = {
title: 'JSON Editor',
id: 'jsoneditor',
title: t('JSON Editor'),
type: 'jsoneditor'
}
return [layerGroup, filterGroup]
.concat(getLayoutForType(layerType).groups)
.concat(getLayoutForType(layerType, t).groups)
.concat([editorGroup])
}
type LayerEditorProps = {
type LayerEditorInternalProps = {
layer: LayerSpecification
sources: {[key: string]: SourceSpecification}
vectorLayers: {[key: string]: any}
@@ -58,14 +72,14 @@ type LayerEditorProps = {
isLastLayer?: boolean
layerIndex: number
errors?: any[]
};
} & WithTranslation;
type LayerEditorState = {
editorGroups: {[keys:string]: boolean}
};
/** Layer editor supporting multiple types of layers. */
export default class LayerEditor extends React.Component<LayerEditorProps, LayerEditorState> {
class LayerEditorInternal extends React.Component<LayerEditorInternalProps, LayerEditorState> {
static defaultProps = {
onLayerChanged: () => {},
onLayerIdChange: () => {},
@@ -76,22 +90,22 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
reactIconBase: PropTypes.object
}
constructor(props: LayerEditorProps) {
constructor(props: LayerEditorInternalProps) {
super(props)
//TODO: Clean this up and refactor into function
const editorGroups: {[keys:string]: boolean} = {}
layoutGroups(this.props.layer.type).forEach(group => {
layoutGroups(this.props.layer.type, props.t).forEach(group => {
editorGroups[group.title] = true
})
this.state = { editorGroups }
}
static getDerivedStateFromProps(props: LayerEditorProps, state: LayerEditorState) {
static getDerivedStateFromProps(props: Readonly<LayerEditorInternalProps>, state: LayerEditorState) {
const additionalGroups = { ...state.editorGroups }
getLayoutForType(props.layer.type).groups.forEach(group => {
getLayoutForType(props.layer.type, props.t).groups.forEach(group => {
if(!(group.title in additionalGroups)) {
additionalGroups[group.title] = true
}
@@ -242,17 +256,19 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
}
render() {
const t = this.props.t;
const groupIds: string[] = [];
const layerType = this.props.layer.type
const groups = layoutGroups(layerType).filter(group => {
const groups = layoutGroups(layerType, t).filter(group => {
return !(layerType === 'background' && group.type === 'source')
}).map(group => {
const groupId = group.title.replace(/ /g, "_");
const groupId = group.id;
groupIds.push(groupId);
return <LayerEditorGroup
data-wd-key={group.title}
id={groupId}
key={group.title}
key={groupId}
title={group.title}
isActive={this.state.editorGroups[group.title]}
onActiveToggle={this.onGroupToggle.bind(this, group.title)}
@@ -265,25 +281,25 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
const items: {[key: string]: {text: string, handler: () => void, disabled?: boolean}} = {
delete: {
text: "Delete",
text: t("Delete"),
handler: () => this.props.onLayerDestroy(this.props.layerIndex)
},
duplicate: {
text: "Duplicate",
text: t("Duplicate"),
handler: () => this.props.onLayerCopy(this.props.layerIndex)
},
hide: {
text: (layout.visibility === "none") ? "Show" : "Hide",
text: (layout.visibility === "none") ? t("Show") : t("Hide"),
handler: () => this.props.onLayerVisibilityToggle(this.props.layerIndex)
},
moveLayerUp: {
text: "Move layer up",
text: t("Move layer up"),
// Not actually used...
disabled: this.props.isFirstLayer,
handler: () => this.moveLayer(-1)
},
moveLayerDown: {
text: "Move layer down",
text: t("Move layer down"),
// Not actually used...
disabled: this.props.isLastLayer,
handler: () => this.moveLayer(+1)
@@ -297,12 +313,12 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
return <section className="maputnik-layer-editor"
role="main"
aria-label="Layer editor"
aria-label={t("Layer editor")}
>
<header>
<div className="layer-header">
<h2 className="layer-header__title">
Layer: {formatLayerId(this.props.layer.id)}
{t("Layer: {{layerId}}", { layerId: formatLayerId(this.props.layer.id) })}
</h2>
<div className="layer-header__info">
<Wrapper
@@ -310,7 +326,11 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
onSelection={handleSelection}
closeOnSelection={false}
>
<Button id="skip-target-layer-editor" data-wd-key="skip-target-layer-editor" className='more-menu__button' title="Layer options">
<Button
id="skip-target-layer-editor"
data-wd-key="skip-target-layer-editor"
className='more-menu__button'
title={"Layer options"}>
<MdMoreVert className="more-menu__button__svg" />
</Button>
<Menu>
@@ -340,3 +360,6 @@ export default class LayerEditor extends React.Component<LayerEditorProps, Layer
</section>
}
}
const LayerEditor = withTranslation()(LayerEditorInternal);
export default LayerEditor;

View File

@@ -10,6 +10,7 @@ import {SortEndHandler, SortableContainer} from 'react-sortable-hoc';
import type {LayerSpecification} from 'maplibre-gl';
import generateUniqueId from '../libs/document-uid';
import { findClosestCommonPrefix, layerPrefix } from '../libs/layer';
import { WithTranslation, withTranslation } from 'react-i18next';
type LayerListContainerProps = {
layers: LayerSpecification[]
@@ -22,6 +23,7 @@ type LayerListContainerProps = {
sources: object
errors: any[]
};
type LayerListContainerInternalProps = LayerListContainerProps & WithTranslation;
type LayerListContainerState = {
collapsedGroups: {[ket: string]: boolean}
@@ -31,14 +33,14 @@ type LayerListContainerState = {
};
// List of collapsible layer editors
class LayerListContainer extends React.Component<LayerListContainerProps, LayerListContainerState> {
class LayerListContainerInternal extends React.Component<LayerListContainerInternalProps, LayerListContainerState> {
static defaultProps = {
onLayerSelect: () => {},
}
selectedItemRef: React.RefObject<any>;
scrollContainerRef: React.RefObject<HTMLElement>;
constructor(props: LayerListContainerProps) {
constructor(props: LayerListContainerInternalProps) {
super(props);
this.selectedItemRef = React.createRef();
this.scrollContainerRef = React.createRef();
@@ -259,10 +261,12 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
})
})
const t = this.props.t;
return <section
className="maputnik-layer-list"
role="complementary"
aria-label="Layers list"
aria-label={t("Layers list")}
ref={this.scrollContainerRef}
>
<ModalAdd
@@ -274,7 +278,7 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
onLayersChange={this.props.onLayersChange}
/>
<header className="maputnik-layer-list-header">
<span className="maputnik-layer-list-header-title">Layers</span>
<span className="maputnik-layer-list-header-title">{t("Layers")}</span>
<span className="maputnik-space" />
<div className="maputnik-default-property">
<div className="maputnik-multibutton">
@@ -283,7 +287,11 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
data-wd-key="skip-target-layer-list"
onClick={this.toggleLayers}
className="maputnik-button">
{this.state.areAllGroupsExpanded === true ? "Collapse" : "Expand"}
{this.state.areAllGroupsExpanded === true ?
t("Collapse")
:
t("Expand")
}
</button>
</div>
</div>
@@ -293,14 +301,14 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
onClick={this.toggleModal.bind(this, 'add')}
data-wd-key="layer-list:add-layer"
className="maputnik-button maputnik-button-selected">
Add Layer
{t("Add Layer")}
</button>
</div>
</div>
</header>
<div
role="navigation"
aria-label="Layers list"
aria-label={t("Layers list")}
>
<ul className="maputnik-layer-list-container">
{listItems}
@@ -310,6 +318,13 @@ class LayerListContainer extends React.Component<LayerListContainerProps, LayerL
}
}
// The next two lines have react-refresh/only-export-components disabled because they are
// internal components that are not intended to be used outside of this file.
// For some reason, the linter is not recognizing these components correctly.
// When these components are migrated to functional components, the HOCs will no longer be needed
// and the comments can be removed.
// eslint-disable-next-line react-refresh/only-export-components
const LayerListContainer = withTranslation()(LayerListContainerInternal);
// eslint-disable-next-line react-refresh/only-export-components
const LayerListContainerSortable = SortableContainer((props: LayerListContainerProps) => <LayerListContainer {...props} />)

View File

@@ -14,6 +14,7 @@ import '../libs/maplibre-rtl'
//@ts-ignore
import MaplibreGeocoder from '@maplibre/maplibre-gl-geocoder';
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css';
import { withTranslation, WithTranslation } from 'react-i18next'
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container): HTMLElement {
ReactDOM.render(popup, mountNode);
@@ -51,7 +52,7 @@ function buildInspectStyle(originalMapStyle: StyleSpecification, coloredLayers:
return inspectStyle
}
type MapMaplibreGlProps = {
type MapMaplibreGlInternalProps = {
onDataChange?(event: {map: Map | null}): unknown
onLayerSelect(...args: unknown[]): unknown
mapStyle: StyleSpecification
@@ -64,7 +65,7 @@ type MapMaplibreGlProps = {
}
replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification
onChange(value: {center: LngLat, zoom: number}): unknown
};
} & WithTranslation;
type MapMaplibreGlState = {
map: Map | null
@@ -72,7 +73,7 @@ type MapMaplibreGlState = {
zoom?: number
};
export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, MapMaplibreGlState> {
class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps, MapMaplibreGlState> {
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
@@ -82,7 +83,7 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
}
container: HTMLDivElement | null = null
constructor(props: MapMaplibreGlProps) {
constructor(props: MapMaplibreGlInternalProps) {
super(props)
this.state = {
map: null,
@@ -91,7 +92,7 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
}
shouldComponentUpdate(nextProps: MapMaplibreGlProps, nextState: MapMaplibreGlState) {
shouldComponentUpdate(nextProps: MapMaplibreGlInternalProps, nextState: MapMaplibreGlState) {
let should = false;
try {
should = JSON.stringify(this.props) !== JSON.stringify(nextProps) || JSON.stringify(this.state) !== JSON.stringify(nextState);
@@ -154,7 +155,7 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
this.initGeocoder(map);
const zoomControl = new ZoomControl;
const zoomControl = new ZoomControl(this.props.t("Zoom:"));
map.addControl(zoomControl, 'top-right');
const nav = new MapLibreGl.NavigationControl({visualizePitch:true});
@@ -256,18 +257,24 @@ export default class MapMaplibreGl extends React.Component<MapMaplibreGlProps, M
};
}
};
const geocoder = new MaplibreGeocoder(geocoderConfig, {maplibregl: MapLibreGl});
const geocoder = new MaplibreGeocoder(geocoderConfig, {
placeholder: this.props.t("Search"),
maplibregl: MapLibreGl,
});
map.addControl(geocoder, 'top-left');
}
render() {
const t = this.props.t;
return <div
className="maputnik-map__map"
role="region"
aria-label="Map view"
aria-label={t("Map view")}
ref={x => this.container = x}
data-wd-key="maplibre:map"
></div>
}
}
const MapMaplibreGl = withTranslation()(MapMaplibreGlInternal);
export default MapMaplibreGl;

View File

@@ -1,5 +1,6 @@
import React from 'react'
import {throttle} from 'lodash';
import { WithTranslation, withTranslation } from 'react-i18next';
import MapMaplibreGlLayerPopup from './MapMaplibreGlLayerPopup';
@@ -23,7 +24,7 @@ function renderCoords (coords: string[]) {
}
}
type MapOpenLayersProps = {
type MapOpenLayersInternalProps = {
onDataChange?(...args: unknown[]): unknown
mapStyle: object
accessToken?: string
@@ -32,7 +33,7 @@ type MapOpenLayersProps = {
debugToolbox: boolean
replaceAccessTokens(...args: unknown[]): unknown
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
type MapOpenLayersState = {
zoom: string
@@ -42,7 +43,7 @@ type MapOpenLayersState = {
selectedFeatures?: any[]
};
export default class MapOpenLayers extends React.Component<MapOpenLayersProps, MapOpenLayersState> {
class MapOpenLayersInternal extends React.Component<MapOpenLayersInternalProps, MapOpenLayersState> {
static defaultProps = {
onMapLoaded: () => {},
onDataChange: () => {},
@@ -54,7 +55,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
overlay: Overlay | undefined;
popupContainer: HTMLElement | null = null;
constructor(props: MapOpenLayersProps) {
constructor(props: MapOpenLayersInternalProps) {
super(props);
this.state = {
zoom: "0",
@@ -73,7 +74,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
apply(this.map, newMapStyle);
}
componentDidUpdate(prevProps: MapOpenLayersProps) {
componentDidUpdate(prevProps: MapOpenLayersInternalProps) {
if (this.props.mapStyle !== prevProps.mapStyle) {
this.updateStyle(
this.props.replaceAccessTokens(this.props.mapStyle)
@@ -151,6 +152,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
}
render() {
const t = this.props.t;
return <div className="maputnik-ol-container">
<div
ref={x => this.popupContainer = x}
@@ -160,7 +162,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
<button
className="maplibregl-popup-close-button"
onClick={this.closeOverlay}
aria-label="Close popup"
aria-label={t("Close popup")}
>
×
</button>
@@ -170,20 +172,20 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
/>
</div>
<div className="maputnik-ol-zoom">
Zoom: {this.state.zoom}
{t("Zoom:")} {this.state.zoom}
</div>
{this.props.debugToolbox &&
<div className="maputnik-ol-debug">
<div>
<label>cursor: </label>
<label>{t("cursor:")} </label>
<span>{renderCoords(this.state.cursor)}</span>
</div>
<div>
<label>center: </label>
<label>{t("center:")} </label>
<span>{renderCoords(this.state.center)}</span>
</div>
<div>
<label>rotation: </label>
<label>{t("rotation:")} </label>
<span>{this.state.rotation}</span>
</div>
</div>
@@ -192,7 +194,7 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
className="maputnik-ol"
ref={x => this.container = x}
role="region"
aria-label="Map view"
aria-label={t("Map view")}
style={{
...this.props.style,
}}>
@@ -201,3 +203,5 @@ export default class MapOpenLayers extends React.Component<MapOpenLayersProps, M
}
}
const MapOpenLayers = withTranslation()(MapOpenLayersInternal);
export default MapOpenLayers;

View File

@@ -2,9 +2,9 @@ import React, { PropsWithChildren } from 'react'
import {MdClose} from 'react-icons/md'
import AriaModal from 'react-aria-modal'
import classnames from 'classnames';
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalProps = PropsWithChildren & {
type ModalInternalProps = PropsWithChildren & {
"data-wd-key"?: string
isOpen: boolean
title: string
@@ -12,10 +12,10 @@ type ModalProps = PropsWithChildren & {
underlayClickExits?: boolean
underlayProps?: any
className?: string
};
} & WithTranslation;
export default class Modal extends React.Component<ModalProps> {
class ModalInternal extends React.Component<ModalInternalProps> {
static defaultProps = {
underlayClickExits: true
}
@@ -32,6 +32,7 @@ export default class Modal extends React.Component<ModalProps> {
}
render() {
const t = this.props.t;
if(this.props.isOpen) {
return <AriaModal
titleText={this.props.title}
@@ -49,7 +50,7 @@ export default class Modal extends React.Component<ModalProps> {
<h1 className="maputnik-modal-header-title">{this.props.title}</h1>
<span className="maputnik-modal-header-space"></span>
<button className="maputnik-modal-header-toggle"
title="Close modal"
title={t("Close modal")}
onClick={this.onClose}
data-wd-key={this.props["data-wd-key"]+".close-modal"}
>
@@ -68,3 +69,5 @@ export default class Modal extends React.Component<ModalProps> {
}
}
const Modal = withTranslation()(ModalInternal);
export default Modal;

View File

@@ -7,15 +7,16 @@ import FieldId from './FieldId'
import FieldSource from './FieldSource'
import FieldSourceLayer from './FieldSourceLayer'
import type {LayerSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalAddProps = {
type ModalAddInternalProps = {
layers: LayerSpecification[]
onLayersChange(layers: LayerSpecification[]): unknown
isOpen: boolean
onOpenToggle(open: boolean): unknown
// A dict of source id's and the available source layers
sources: any
};
} & WithTranslation;
type ModalAddState = {
type: LayerSpecification["type"]
@@ -24,7 +25,7 @@ type ModalAddState = {
'source-layer'?: string
};
export default class ModalAdd extends React.Component<ModalAddProps, ModalAddState> {
class ModalAddInternal extends React.Component<ModalAddInternalProps, ModalAddState> {
addLayer = () => {
const changedLayers = this.props.layers.slice(0)
const layer: ModalAddState = {
@@ -45,7 +46,7 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
this.props.onOpenToggle(false)
}
constructor(props: ModalAddProps) {
constructor(props: ModalAddInternalProps) {
super(props)
const state: ModalAddState = {
type: 'fill',
@@ -54,12 +55,12 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
if(props.sources.length > 0) {
state.source = Object.keys(this.props.sources)[0];
state['source-layer'] = this.props.sources[state.source as keyof ModalAddProps["sources"]][0]
state['source-layer'] = this.props.sources[state.source as keyof ModalAddInternalProps["sources"]][0]
}
this.state = state;
}
componentDidUpdate(_prevProps: ModalAddProps, prevState: ModalAddState) {
componentDidUpdate(_prevProps: ModalAddInternalProps, prevState: ModalAddState) {
// Check if source is valid for new type
const oldType = prevState.type;
const newType = this.state.type;
@@ -125,13 +126,14 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
render() {
const t = this.props.t;
const sources = this.getSources(this.state.type);
const layers = this.getLayersForSource(this.state.source!);
return <Modal
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Add Layer'}
title={t('Add Layer')}
data-wd-key="modal:add-layer"
className="maputnik-add-modal"
>
@@ -169,10 +171,12 @@ export default class ModalAdd extends React.Component<ModalAddProps, ModalAddSta
onClick={this.addLayer}
data-wd-key="add-layer"
>
Add Layer
{t("Add Layer")}
</InputButton>
</div>
</Modal>
}
}
const ModalAdd = withTranslation()(ModalAddInternal);
export default ModalAdd;

View File

@@ -1,9 +1,10 @@
import React from 'react'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal'
type ModalDebugProps = {
type ModalDebugInternalProps = {
isOpen: boolean
renderer: string
onChangeMaplibreGlDebug(key: string, checked: boolean): unknown
@@ -18,12 +19,12 @@ type ModalDebugProps = {
lat: number
}
}
};
} & WithTranslation;
export default class ModalDebug extends React.Component<ModalDebugProps> {
class ModalDebugInternal extends React.Component<ModalDebugInternalProps> {
render() {
const {mapView} = this.props;
const {t, mapView} = this.props;
const osmZoom = Math.round(mapView.zoom)+1;
const osmLon = +(mapView.center.lng).toFixed(5);
@@ -33,10 +34,10 @@ export default class ModalDebug extends React.Component<ModalDebugProps> {
data-wd-key="modal:debug"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Debug'}
title={t('Debug')}
>
<section className="maputnik-modal-section maputnik-modal-shortcuts">
<h1>Options</h1>
<h1>{t("Options")}</h1>
{this.props.renderer === 'mlgljs' &&
<ul>
{Object.entries(this.props.maplibreGlDebugOptions!).map(([key, val]) => {
@@ -63,16 +64,20 @@ export default class ModalDebug extends React.Component<ModalDebugProps> {
<section className="maputnik-modal-section">
<h1>Links</h1>
<p>
<a
target="_blank"
rel="noopener noreferrer"
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
>
Open in OSM
</a> &mdash; Opens the current view on openstreetmap.org
<Trans t={t}>
<a
target="_blank"
rel="noopener noreferrer"
href={`https://www.openstreetmap.org/#map=${osmZoom}/${osmLat}/${osmLon}`}
>
Open in OSM
</a> &mdash; Opens the current view on openstreetmap.org
</Trans>
</p>
</section>
</Modal>
}
}
const ModalDebug = withTranslation()(ModalDebugInternal);
export default ModalDebug;

View File

@@ -5,6 +5,7 @@ import {version} from 'maplibre-gl/package.json'
import {format} from '@maplibre/maplibre-gl-style-spec'
import type {StyleSpecification} from 'maplibre-gl'
import {MdFileDownload} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
import FieldString from './FieldString'
import InputButton from './InputButton'
@@ -16,15 +17,15 @@ import fieldSpecAdditional from '../libs/field-spec-additional'
const MAPLIBRE_GL_VERSION = version;
type ModalExportProps = {
type ModalExportInternalProps = {
mapStyle: StyleSpecification & { id: string }
onStyleChanged(...args: unknown[]): unknown
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
};
} & WithTranslation;
export default class ModalExport extends React.Component<ModalExportProps> {
class ModalExportInternal extends React.Component<ModalExportInternalProps> {
tokenizedStyle () {
return format(
@@ -48,7 +49,7 @@ export default class ModalExport extends React.Component<ModalExportProps> {
downloadHtml() {
const tokenStyle = this.tokenizedStyle();
const htmlTitle = this.props.mapStyle.name || "Map";
const htmlTitle = this.props.mapStyle.name || this.props.t("Map");
const html = `<!DOCTYPE html>
<html>
<head>
@@ -100,18 +101,20 @@ export default class ModalExport extends React.Component<ModalExportProps> {
render() {
const t = this.props.t;
const fsa = fieldSpecAdditional(t);
return <Modal
data-wd-key="modal:export"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Export Style'}
title={t('Export Style')}
className="maputnik-export-modal"
>
<section className="maputnik-modal-section">
<h1>Download Style</h1>
<h1>{t("Download Style")}</h1>
<p>
Download a JSON style to your computer.
{t("Download a JSON style to your computer.")}
</p>
<p>
<a href="https://docs.maptiler.com/cloud/api/authentication-key/" target="_blank" rel="noreferrer">MapTiler</a>,&nbsp;
@@ -123,14 +126,14 @@ export default class ModalExport extends React.Component<ModalExportProps> {
<div>
<FieldString
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
label={fsa.maputnik.maptiler_access_token.label}
fieldSpec={fsa.maputnik.maptiler_access_token}
value={(this.props.mapStyle.metadata || {} as any)['maputnik:openmaptiles_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/>
<FieldString
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
label={fsa.maputnik.thunderforest_access_token.label}
fieldSpec={fsa.maputnik.thunderforest_access_token}
value={(this.props.mapStyle.metadata || {} as any)['maputnik:thunderforest_access_token']}
onChange={this.changeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/>
@@ -141,14 +144,14 @@ export default class ModalExport extends React.Component<ModalExportProps> {
onClick={this.downloadStyle.bind(this)}
>
<MdFileDownload />
Download Style
{t("Download Style")}
</InputButton>
<InputButton
onClick={this.downloadHtml.bind(this)}
>
<MdFileDownload />
Download HTML
{t("Download HTML")}
</InputButton>
</div>
</section>
@@ -157,3 +160,5 @@ export default class ModalExport extends React.Component<ModalExportProps> {
}
}
const ModalExport = withTranslation()(ModalExportInternal);
export default ModalExport;

View File

@@ -2,23 +2,25 @@ import React from 'react'
import InputButton from './InputButton'
import Modal from './Modal'
import { WithTranslation, withTranslation } from 'react-i18next';
type ModalLoadingProps = {
type ModalLoadingInternalProps = {
isOpen: boolean
onCancel(...args: unknown[]): unknown
title: string
message: React.ReactNode
};
} & WithTranslation;
export default class ModalLoading extends React.Component<ModalLoadingProps> {
class ModalLoadingInternal extends React.Component<ModalLoadingInternalProps> {
underlayOnClick(e: Event) {
// This stops click events falling through to underlying modals.
e.stopPropagation();
}
render() {
const t = this.props.t;
return <Modal
data-wd-key="modal:loading"
isOpen={this.props.isOpen}
@@ -35,10 +37,12 @@ export default class ModalLoading extends React.Component<ModalLoadingProps> {
</p>
<p className="maputnik-dialog__buttons">
<InputButton onClick={(e) => this.props.onCancel(e)}>
Cancel
{t("Cancel")}
</InputButton>
</p>
</Modal>
}
}
const ModalLoading = withTranslation()(ModalLoadingInternal);
export default ModalLoading;

View File

@@ -2,6 +2,7 @@ import React, { FormEvent } from 'react'
import {MdFileUpload} from 'react-icons/md'
import {MdAddCircleOutline} from 'react-icons/md'
import FileReaderInput, { Result } from 'react-file-reader-input'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import ModalLoading from './ModalLoading'
import Modal from './Modal'
@@ -42,11 +43,11 @@ class PublicStyle extends React.Component<PublicStyleProps> {
}
}
type ModalOpenProps = {
type ModalOpenInternalProps = {
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
onStyleOpen(...args: unknown[]): unknown
};
} & WithTranslation;
type ModalOpenState = {
styleUrl: string
@@ -55,8 +56,8 @@ type ModalOpenState = {
activeRequestUrl?: string | null
};
export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpenState> {
constructor(props: ModalOpenProps) {
class ModalOpenInternal extends React.Component<ModalOpenInternalProps, ModalOpenState> {
constructor(props: ModalOpenInternalProps) {
super(props);
this.state = {
styleUrl: ""
@@ -174,6 +175,7 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
}
render() {
const t = this.props.t;
const styleOptions = publicStyles.map(style => {
return <PublicStyle
key={style.id}
@@ -200,29 +202,31 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
data-wd-key="modal:open"
isOpen={this.props.isOpen}
onOpenToggle={() => this.onOpenToggle()}
title={'Open Style'}
title={t('Open Style')}
>
{errorElement}
<section className="maputnik-modal-section">
<h1>Upload Style</h1>
<p>Upload a JSON style from your computer.</p>
<FileReaderInput onChange={this.onUpload} tabIndex={-1} aria-label="Style file">
<InputButton className="maputnik-upload-button"><MdFileUpload /> Upload</InputButton>
<h1>{t("Upload Style")}</h1>
<p>{t("Upload a JSON style from your computer.")}</p>
<FileReaderInput onChange={this.onUpload} tabIndex={-1} aria-label={t("Style file")}>
<InputButton className="maputnik-upload-button"><MdFileUpload /> {t("Upload")}</InputButton>
</FileReaderInput>
</section>
<section className="maputnik-modal-section">
<form onSubmit={this.onSubmitUrl}>
<h1>Load from URL</h1>
<h1>{t("Load from URL")}</h1>
<p>
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
<Trans t={t}>
Load from a URL. Note that the URL must have <a href="https://enable-cors.org" target="_blank" rel="noopener noreferrer">CORS enabled</a>.
</Trans>
</p>
<InputUrl
aria-label="Style URL"
aria-label={t("Style URL")}
data-wd-key="modal:open.url.input"
type="text"
className="maputnik-input"
default="Enter URL..."
default={t("Enter URL...")}
value={this.state.styleUrl}
onInput={this.onChangeUrl}
onChange={this.onChangeUrl}
@@ -239,9 +243,9 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
</section>
<section className="maputnik-modal-section maputnik-modal-section--shrink">
<h1>Gallery Styles</h1>
<h1>{t("Gallery Styles")}</h1>
<p>
Open one of the publicly available styles to start from.
{t("Open one of the publicly available styles to start from.")}
</p>
<div className="maputnik-style-gallery-container">
{styleOptions}
@@ -251,12 +255,14 @@ export default class ModalOpen extends React.Component<ModalOpenProps, ModalOpen
<ModalLoading
isOpen={!!this.state.activeRequest}
title={'Loading style'}
title={t('Loading style')}
onCancel={(e: Event) => this.onCancelActiveRequest(e)}
message={"Loading: "+this.state.activeRequestUrl}
message={t("Loading: {{requestUrl}}", { requestUrl: this.state.activeRequestUrl })}
/>
</div>
)
}
}
const ModalOpen = withTranslation()(ModalOpenInternal);
export default ModalOpen;

View File

@@ -1,6 +1,7 @@
import React from 'react'
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import type {LightSpecification, StyleSpecification, TerrainSpecification, TransitionSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
import FieldArray from './FieldArray'
import FieldNumber from './FieldNumber'
@@ -12,15 +13,15 @@ import FieldColor from './FieldColor'
import Modal from './Modal'
import fieldSpecAdditional from '../libs/field-spec-additional'
type ModalSettingsProps = {
type ModalSettingsInternalProps = {
mapStyle: StyleSpecification
onStyleChanged(...args: unknown[]): unknown
onChangeMetadataProperty(...args: unknown[]): unknown
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
};
} & WithTranslation;
export default class ModalSettings extends React.Component<ModalSettingsProps> {
class ModalSettingsInternal extends React.Component<ModalSettingsInternalProps> {
changeTransitionProperty(property: keyof TransitionSpecification, value: number | undefined) {
const transition = {
...this.props.mapStyle.transition,
@@ -95,7 +96,8 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
render() {
const metadata = this.props.mapStyle.metadata || {} as any;
const {onChangeMetadataProperty, mapStyle} = this.props;
const {t, onChangeMetadataProperty, mapStyle} = this.props;
const fsa = fieldSpecAdditional(t);
const light = this.props.mapStyle.light || {};
const transition = this.props.mapStyle.transition || {};
@@ -105,33 +107,33 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
data-wd-key="modal:settings"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Style Settings'}
title={t('Style Settings')}
>
<div className="modal:settings">
<FieldString
label={"Name"}
label={t("Name")}
fieldSpec={latest.$root.name}
data-wd-key="modal:settings.name"
value={this.props.mapStyle.name}
onChange={this.changeStyleProperty.bind(this, "name")}
/>
<FieldString
label={"Owner"}
fieldSpec={{doc: "Owner ID of the style. Used by Mapbox or future style APIs."}}
label={t("Owner")}
fieldSpec={{doc: t("Owner ID of the style. Used by Mapbox or future style APIs.")}}
data-wd-key="modal:settings.owner"
value={(this.props.mapStyle as any).owner}
onChange={this.changeStyleProperty.bind(this, "owner")}
/>
<FieldUrl
fieldSpec={latest.$root.sprite}
label="Sprite URL"
label={t("Sprite URL")}
data-wd-key="modal:settings.sprite"
value={this.props.mapStyle.sprite as string}
onChange={this.changeStyleProperty.bind(this, "sprite")}
/>
<FieldUrl
label="Glyphs URL"
label={t("Glyphs URL")}
fieldSpec={latest.$root.glyphs}
data-wd-key="modal:settings.glyphs"
value={this.props.mapStyle.glyphs as string}
@@ -139,23 +141,23 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldString
label={fieldSpecAdditional.maputnik.maptiler_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.maptiler_access_token}
label={fsa.maputnik.maptiler_access_token.label}
fieldSpec={fsa.maputnik.maptiler_access_token}
data-wd-key="modal:settings.maputnik:openmaptiles_access_token"
value={metadata['maputnik:openmaptiles_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:openmaptiles_access_token")}
/>
<FieldString
label={fieldSpecAdditional.maputnik.thunderforest_access_token.label}
fieldSpec={fieldSpecAdditional.maputnik.thunderforest_access_token}
label={fsa.maputnik.thunderforest_access_token.label}
fieldSpec={fsa.maputnik.thunderforest_access_token}
data-wd-key="modal:settings.maputnik:thunderforest_access_token"
value={metadata['maputnik:thunderforest_access_token']}
onChange={onChangeMetadataProperty.bind(this, "maputnik:thunderforest_access_token")}
/>
<FieldArray
label={"Center"}
label={t("Center")}
fieldSpec={latest.$root.center}
length={2}
type="number"
@@ -165,7 +167,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldNumber
label={"Zoom"}
label={t("Zoom")}
fieldSpec={latest.$root.zoom}
value={mapStyle.zoom}
default={0}
@@ -173,7 +175,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldNumber
label={"Bearing"}
label={t("Bearing")}
fieldSpec={latest.$root.bearing}
value={mapStyle.bearing}
default={latest.$root.bearing.default}
@@ -181,7 +183,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldNumber
label={"Pitch"}
label={t("Pitch")}
fieldSpec={latest.$root.pitch}
value={mapStyle.pitch}
default={latest.$root.pitch.default}
@@ -189,7 +191,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldEnum
label={"Light anchor"}
label={t("Light anchor")}
fieldSpec={latest.light.anchor}
name="light-anchor"
value={light.anchor as string}
@@ -199,7 +201,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldColor
label={"Light color"}
label={t("Light color")}
fieldSpec={latest.light.color}
value={light.color as string}
default={latest.light.color.default}
@@ -207,7 +209,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldNumber
label={"Light intensity"}
label={t("Light intensity")}
fieldSpec={latest.light.intensity}
value={light.intensity as number}
default={latest.light.intensity.default}
@@ -215,7 +217,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldArray
label={"Light position"}
label={t("Light position")}
fieldSpec={latest.light.position}
type="number"
length={latest.light.position.length}
@@ -225,7 +227,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldString
label={"Terrain source"}
label={t("Terrain source")}
fieldSpec={latest.terrain.source}
data-wd-key="modal:settings.maputnik:terrain_source"
value={terrain.source}
@@ -233,7 +235,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldNumber
label={"Terrain exaggeration"}
label={t("Terrain exaggeration")}
fieldSpec={latest.terrain.exaggeration}
value={terrain.exaggeration}
default={latest.terrain.exaggeration.default}
@@ -241,7 +243,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldNumber
label={"Transition delay"}
label={t("Transition delay")}
fieldSpec={latest.transition.delay}
value={transition.delay}
default={latest.transition.delay.default}
@@ -249,7 +251,7 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldNumber
label={"Transition duration"}
label={t("Transition duration")}
fieldSpec={latest.transition.duration}
value={transition.duration}
default={latest.transition.duration.default}
@@ -257,12 +259,12 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
/>
<FieldSelect
label={fieldSpecAdditional.maputnik.style_renderer.label}
fieldSpec={fieldSpecAdditional.maputnik.style_renderer}
label={fsa.maputnik.style_renderer.label}
fieldSpec={fsa.maputnik.style_renderer}
data-wd-key="modal:settings.maputnik:renderer"
options={[
['mlgljs', 'MapLibreGL JS'],
['ol', 'Open Layers (experimental)'],
['ol', t('Open Layers (experimental)')],
]}
value={metadata['maputnik:renderer'] || 'mlgljs'}
onChange={onChangeMetadataProperty.bind(this, 'maputnik:renderer')}
@@ -272,3 +274,5 @@ export default class ModalSettings extends React.Component<ModalSettingsProps> {
}
}
const ModalSettings = withTranslation()(ModalSettingsInternal)
export default ModalSettings;

View File

@@ -1,48 +1,50 @@
import React from 'react'
import { Trans, WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal'
type ModalShortcutsProps = {
type ModalShortcutsInternalProps = {
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
};
} & WithTranslation;
export default class ModalShortcuts extends React.Component<ModalShortcutsProps> {
class ModalShortcutsInternal extends React.Component<ModalShortcutsInternalProps> {
render() {
const t = this.props.t;
const help = [
{
key: <kbd>?</kbd>,
text: "Shortcuts menu"
text: t("Shortcuts menu")
},
{
key: <kbd>o</kbd>,
text: "Open modal"
text: t("Open modal")
},
{
key: <kbd>e</kbd>,
text: "Export modal"
text: t("Export modal")
},
{
key: <kbd>d</kbd>,
text: "Data Sources modal"
text: t("Data Sources modal")
},
{
key: <kbd>s</kbd>,
text: "Style Settings modal"
text: t("Style Settings modal")
},
{
key: <kbd>i</kbd>,
text: "Toggle inspect"
text: t("Toggle inspect")
},
{
key: <kbd>m</kbd>,
text: "Focus map"
text: t("Focus map")
},
{
key: <kbd>!</kbd>,
text: "Debug modal"
text: t("Debug modal")
},
]
@@ -50,51 +52,51 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
const mapShortcuts = [
{
key: <kbd>+</kbd>,
text: "Increase the zoom level by 1.",
text: t("Increase the zoom level by 1.",)
},
{
key: <><kbd>Shift</kbd> + <kbd>+</kbd></>,
text: "Increase the zoom level by 2.",
text: t("Increase the zoom level by 2.",)
},
{
key: <kbd>-</kbd>,
text: "Decrease the zoom level by 1.",
text: t("Decrease the zoom level by 1.",)
},
{
key: <><kbd>Shift</kbd> + <kbd>-</kbd></>,
text: "Decrease the zoom level by 2.",
text: t("Decrease the zoom level by 2.",)
},
{
key: <kbd>Up</kbd>,
text: "Pan up by 100 pixels.",
text: t("Pan up by 100 pixels.",)
},
{
key: <kbd>Down</kbd>,
text: "Pan down by 100 pixels.",
text: t("Pan down by 100 pixels.",)
},
{
key: <kbd>Left</kbd>,
text: "Pan left by 100 pixels.",
text: t("Pan left by 100 pixels.",)
},
{
key: <kbd>Right</kbd>,
text: "Pan right by 100 pixels.",
text: t("Pan right by 100 pixels.",)
},
{
key: <><kbd>Shift</kbd> + <kbd>Right</kbd></>,
text: "Increase the rotation by 15 degrees.",
text: t("Increase the rotation by 15 degrees.",)
},
{
key: <><kbd>Shift</kbd> + <kbd>Left</kbd></>,
text: "Decrease the rotation by 15 degrees."
text: t("Decrease the rotation by 15 degrees.")
},
{
key: <><kbd>Shift</kbd> + <kbd>Up</kbd></>,
text: "Increase the pitch by 10 degrees."
text: t("Increase the pitch by 10 degrees.")
},
{
key: <><kbd>Shift</kbd> + <kbd>Down</kbd></>,
text: "Decrease the pitch by 10 degrees."
text: t("Decrease the pitch by 10 degrees.")
},
]
@@ -103,11 +105,13 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
data-wd-key="modal:shortcuts"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Shortcuts'}
title={t('Shortcuts')}
>
<section className="maputnik-modal-section maputnik-modal-shortcuts">
<p>
Press <code>ESC</code> to lose focus of any active elements, then press one of:
<Trans t={t}>
Press <code>ESC</code> to lose focus of any active elements, then press one of:
</Trans>
</p>
<dl>
{help.map((item, idx) => {
@@ -117,7 +121,7 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
</div>
})}
</dl>
<p>If the Map is in focused you can use the following shortcuts</p>
<p>{t("If the Map is in focused you can use the following shortcuts")}</p>
<ul>
{mapShortcuts.map((item, idx) => {
return <li key={idx}>
@@ -130,3 +134,5 @@ export default class ModalShortcuts extends React.Component<ModalShortcutsProps>
}
}
const ModalShortcuts = withTranslation()(ModalShortcutsInternal);
export default ModalShortcuts;

View File

@@ -2,6 +2,7 @@ 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, StyleSpecification, VectorSourceSpecification} from 'maplibre-gl'
import { WithTranslation, withTranslation } from 'react-i18next';
import Modal from './Modal'
import InputButton from './InputButton'
@@ -74,16 +75,17 @@ type ActiveModalSourcesTypeEditorProps = {
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={`Remove '${this.props.sourceId}' source`}
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'}}
@@ -104,7 +106,7 @@ class ActiveModalSourcesTypeEditor extends React.Component<ActiveModalSourcesTyp
type AddSourceProps = {
onAdd(...args: unknown[]): unknown
};
} & WithTranslation;
type AddSourceState = {
mode: EditorMode
@@ -202,6 +204,7 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
}
render() {
const t = this.props.t;
// Kind of a hack because the type changes, however maputnik has 1..n
// options per type, for example
//
@@ -215,25 +218,25 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
return <div className="maputnik-add-source">
<FieldString
label={"Source ID"}
fieldSpec={{doc: "Unique ID that identifies the source and is used in the layer to reference the source."}}
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})}
/>
<FieldSelect
label={"Source Type"}
label={t("Source Type")}
fieldSpec={sourceTypeFieldSpec}
options={[
['geojson_json', 'GeoJSON (JSON)'],
['geojson_url', 'GeoJSON (URL)'],
['tilejson_vector', 'Vector (TileJSON URL)'],
['tilexyz_vector', 'Vector (XYZ URLs)'],
['tilejson_raster', 'Raster (TileJSON URL)'],
['tilexyz_raster', 'Raster (XYZ URL)'],
['tilejson_raster-dem', 'Raster DEM (TileJSON URL)'],
['tilexyz_raster-dem', 'Raster DEM (XYZ URLs)'],
['image', 'Image'],
['video', 'Video'],
['geojson_json', t('GeoJSON (JSON)')],
['geojson_url', t('GeoJSON (URL)')],
['tilejson_vector', t('Vector (TileJSON URL)')],
['tilexyz_vector', t('Vector (XYZ URLs)')],
['tilejson_raster', t('Raster (TileJSON URL)')],
['tilexyz_raster', t('Raster (XYZ URL)')],
['tilejson_raster-dem', t('Raster DEM (TileJSON URL)')],
['tilexyz_raster-dem', t('Raster DEM (XYZ URLs)')],
['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}
@@ -247,20 +250,20 @@ class AddSource extends React.Component<AddSourceProps, AddSourceState> {
className="maputnik-add-source-button"
onClick={this.onAdd}
>
Add Source
{t("Add Source")}
</InputButton>
</div>
}
}
type ModalSourcesProps = {
type ModalSourcesInternalProps = {
mapStyle: StyleSpecification
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
onStyleChanged(...args: unknown[]): unknown
};
} & WithTranslation;
export default class ModalSources extends React.Component<ModalSourcesProps> {
class ModalSourcesInternal extends React.Component<ModalSourcesInternalProps> {
stripTitle(source: SourceSpecification & {title?: string}): SourceSpecification {
const strippedSource = {...source}
delete strippedSource['title']
@@ -268,7 +271,8 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
}
render() {
const mapStyle = this.props.mapStyle
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
@@ -277,6 +281,7 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
source={source}
onChange={(src: SourceSpecification) => this.props.onStyleChanged(changeSource(mapStyle, sourceId, src))}
onDelete={() => this.props.onStyleChanged(deleteSource(mapStyle, sourceId))}
{...i18nProps}
/>
})
@@ -295,17 +300,17 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
data-wd-key="modal:sources"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title={'Sources'}
title={t('Sources')}
>
<section className="maputnik-modal-section">
<h1>Active Sources</h1>
<h1>{t("Active Sources")}</h1>
{activeSources}
</section>
<section className="maputnik-modal-section">
<h1>Choose Public Source</h1>
<h1>{t("Choose Public Source")}</h1>
<p>
Add one of the publicly available sources to your style.
{t("Add one of the publicly available sources to your style.")}
</p>
<div className="maputnik-public-sources" style={{maxWidth: 500}}>
{tilesetOptions}
@@ -313,13 +318,16 @@ export default class ModalSources extends React.Component<ModalSourcesProps> {
</section>
<section className="maputnik-modal-section">
<h1>Add New Source</h1>
<p>Add a new source to your style. You can only choose the source type and id at creation time!</p>
<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>
}
}
const ModalSources = withTranslation()(ModalSourcesInternal);
export default ModalSources;

View File

@@ -8,6 +8,8 @@ import FieldDynamicArray from './FieldDynamicArray'
import FieldArray from './FieldArray'
import FieldJson from './FieldJson'
import FieldCheckbox from './FieldCheckbox'
import { WithTranslation, withTranslation } from 'react-i18next';
import { TFunction } from 'i18next'
export type EditorMode = "video" | "image" | "tilejson_vector" | "tilexyz_raster" | "tilejson_raster" | "tilexyz_raster-dem" | "tilejson_raster-dem" | "tilexyz_vector" | "geojson_url" | "geojson_json" | null;
@@ -17,14 +19,15 @@ type TileJSONSourceEditorProps = {
}
onChange(...args: unknown[]): unknown
children?: React.ReactNode
};
} & WithTranslation;
class TileJSONSourceEditor extends React.Component<TileJSONSourceEditorProps> {
render() {
const t = this.props.t;
return <div>
<FieldUrl
label={"TileJSON URL"}
label={t("TileJSON URL")}
fieldSpec={latest.source_vector.url}
value={this.props.source.url}
onChange={url => this.props.onChange({
@@ -45,7 +48,7 @@ type TileURLSourceEditorProps = {
}
onChange(...args: unknown[]): unknown
children?: React.ReactNode
};
} & WithTranslation;
class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
changeTileUrls(tiles: string[]) {
@@ -58,7 +61,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
renderTileUrls() {
const tiles = this.props.source.tiles || [];
return <FieldDynamicArray
label={"Tile URL"}
label={this.props.t("Tile URL")}
fieldSpec={latest.source_vector.tiles}
type="url"
value={tiles}
@@ -67,10 +70,11 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
}
render() {
const t = this.props.t;
return <div>
{this.renderTileUrls()}
<FieldNumber
label={"Min Zoom"}
label={t("Min Zoom")}
fieldSpec={latest.source_vector.minzoom}
value={this.props.source.minzoom || 0}
onChange={minzoom => this.props.onChange({
@@ -79,7 +83,7 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
})}
/>
<FieldNumber
label={"Max Zoom"}
label={t("Max Zoom")}
fieldSpec={latest.source_vector.maxzoom}
value={this.props.source.maxzoom || 22}
onChange={maxzoom => this.props.onChange({
@@ -93,16 +97,24 @@ class TileURLSourceEditor extends React.Component<TileURLSourceEditorProps> {
}
}
const createCornerLabels: (t: TFunction) => { label: string, key: string }[] = (t) => ([
{ label: t("Coord top left"), key: "top left" },
{ label: t("Coord top right"), key: "top right" },
{ label: t("Coord bottom right"), key: "bottom right" },
{ label: t("Coord bottom left"), key: "bottom left" },
]);
type ImageSourceEditorProps = {
source: {
coordinates: [number, number][]
url: string
}
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
render() {
const t = this.props.t;
const changeCoord = (idx: number, val: [number, number]) => {
const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val;
@@ -115,7 +127,7 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
return <div>
<FieldUrl
label={"Image URL"}
label={t("Image URL")}
fieldSpec={latest.source_image.url}
value={this.props.source.url}
onChange={url => this.props.onChange({
@@ -123,11 +135,11 @@ class ImageSourceEditor extends React.Component<ImageSourceEditorProps> {
url,
})}
/>
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
{createCornerLabels(t).map(({label, key}, idx) => {
return (
<FieldArray
label={`Coord ${label}`}
key={label}
label={label}
key={key}
length={2}
type="number"
value={this.props.source.coordinates[idx]}
@@ -146,10 +158,11 @@ type VideoSourceEditorProps = {
urls: string[]
}
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
render() {
const t = this.props.t;
const changeCoord = (idx: number, val: [number, number]) => {
const coordinates = this.props.source.coordinates.slice(0);
coordinates[idx] = val;
@@ -169,18 +182,18 @@ class VideoSourceEditor extends React.Component<VideoSourceEditorProps> {
return <div>
<FieldDynamicArray
label={"Video URL"}
label={t("Video URL")}
fieldSpec={latest.source_video.urls}
type="string"
value={this.props.source.urls}
default={[]}
onChange={changeUrls}
/>
{["top left", "top right", "bottom right", "bottom left"].map((label, idx) => {
{createCornerLabels(t).map(({label, key}, idx) => {
return (
<FieldArray
label={`Coord ${label}`}
key={label}
label={label}
key={key}
length={2}
type="number"
value={this.props.source.coordinates[idx]}
@@ -198,12 +211,13 @@ type GeoJSONSourceUrlEditorProps = {
data: string
}
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
class GeoJSONSourceUrlEditor extends React.Component<GeoJSONSourceUrlEditorProps> {
render() {
const t = this.props.t;
return <FieldUrl
label={"GeoJSON URL"}
label={t("GeoJSON URL")}
fieldSpec={latest.source_geojson.data}
value={this.props.source.data}
onChange={data => this.props.onChange({
@@ -220,12 +234,13 @@ type GeoJSONSourceFieldJsonEditorProps = {
cluster: boolean
}
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJsonEditorProps> {
render() {
const t = this.props.t;
return <div>
<Block label={"GeoJSON"} fieldSpec={latest.source_geojson.data}>
<Block label={t("GeoJSON")} fieldSpec={latest.source_geojson.data}>
<FieldJson
layer={this.props.source.data}
maxHeight={200}
@@ -243,7 +258,7 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
/>
</Block>
<FieldCheckbox
label={'Cluster'}
label={t('Cluster')}
value={this.props.source.cluster}
onChange={cluster => {
this.props.onChange({
@@ -256,18 +271,22 @@ class GeoJSONSourceFieldJsonEditor extends React.Component<GeoJSONSourceFieldJso
}
}
type ModalSourcesTypeEditorProps = {
type ModalSourcesTypeEditorInternalProps = {
mode: EditorMode
source: any
onChange(...args: unknown[]): unknown
};
} & WithTranslation;
export default class ModalSourcesTypeEditor extends React.Component<ModalSourcesTypeEditorProps> {
class ModalSourcesTypeEditorInternal extends React.Component<ModalSourcesTypeEditorInternalProps> {
render() {
const t = this.props.t;
const commonProps = {
source: this.props.source,
onChange: this.props.onChange,
}
t: this.props.t,
i18n: this.props.i18n,
tReady: this.props.tReady,
};
switch(this.props.mode) {
case 'geojson_url': return <GeoJSONSourceUrlEditor {...commonProps} />
case 'geojson_json': return <GeoJSONSourceFieldJsonEditor {...commonProps} />
@@ -278,7 +297,7 @@ export default class ModalSourcesTypeEditor extends React.Component<ModalSources
case 'tilejson_raster-dem': return <TileJSONSourceEditor {...commonProps} />
case 'tilexyz_raster-dem': return <TileURLSourceEditor {...commonProps}>
<FieldSelect
label={"Encoding"}
label={t("Encoding")}
fieldSpec={latest.source_raster_dem.encoding}
options={Object.keys(latest.source_raster_dem.encoding.values)}
onChange={encoding => this.props.onChange({
@@ -295,3 +314,5 @@ export default class ModalSourcesTypeEditor extends React.Component<ModalSources
}
}
const ModalSourcesTypeEditor = withTranslation()(ModalSourcesTypeEditorInternal);
export default ModalSourcesTypeEditor;

View File

@@ -1,38 +0,0 @@
import React from 'react'
import InputButton from './InputButton'
import Modal from './Modal'
// @ts-ignore
import logoImage from 'maputnik-design/logos/logo-color.svg'
type ModalSurveyProps = {
isOpen: boolean
onOpenToggle(...args: unknown[]): unknown
};
export default class ModalSurvey extends React.Component<ModalSurveyProps> {
onClick = () => {
window.open('https://gregorywolanski.typeform.com/to/cPgaSY', '_blank');
this.props.onOpenToggle();
}
render() {
return <Modal
data-wd-key="modal:survey"
isOpen={this.props.isOpen}
onOpenToggle={this.props.onOpenToggle}
title="Maputnik Survey"
>
<div className="maputnik-modal-survey">
<img src={logoImage} className="maputnik-modal-survey__logo" />
<h1>You + Maputnik = Maputnik better for you</h1>
<p className="maputnik-modal-survey__description">We dont track you, so we dont know how you use Maputnik. Help us make Maputnik better for you by completing a 7minute survey carried out by our contributing designer.</p>
<InputButton onClick={this.onClick} className="maputnik-big-button maputnik-white-button maputnik-wide-button">Take the Maputnik Survey</InputButton>
<p className="maputnik-modal-survey__footnote">It takes 7 minutes, tops! Every question is optional.</p>
</div>
</Modal>
}
}

View File

@@ -1,18 +1,22 @@
import React from 'react'
import { WithTranslation, withTranslation } from 'react-i18next';
import './SmallError.scss';
type SmallErrorProps = {
type SmallErrorInternalProps = {
children?: React.ReactNode
};
} & WithTranslation;
export default class SmallError extends React.Component<SmallErrorProps> {
class SmallErrorInternal extends React.Component<SmallErrorInternalProps> {
render () {
const t = this.props.t;
return (
<div className="SmallError">
Error: {this.props.children}
{t("Error:")} {this.props.children}
</div>
);
}
}
}
const SmallError = withTranslation()(SmallErrorInternal);
export default SmallError;

View File

@@ -11,13 +11,14 @@ import Block from './Block'
import docUid from '../libs/document-uid'
import sortNumerically from '../libs/sort-numerically'
import {findDefaultFromSpec} from '../libs/spec-helper';
import { WithTranslation, withTranslation } from 'react-i18next';
import labelFromFieldName from '../libs/label-from-field-name'
import DeleteStopButton from './_DeleteStopButton'
function setStopRefs(props: DataPropertyProps, state: DataPropertyState) {
function setStopRefs(props: DataPropertyInternalProps, state: DataPropertyState) {
// This is initialsed below only if required to improved performance.
let newRefs: {[key: number]: string} | undefined;
@@ -35,7 +36,7 @@ function setStopRefs(props: DataPropertyProps, state: DataPropertyState) {
return newRefs;
}
type DataPropertyProps = {
type DataPropertyInternalProps = {
onChange?(fieldName: string, value: any): unknown
onDeleteStop?(...args: unknown[]): unknown
onAddStop?(...args: unknown[]): unknown
@@ -46,7 +47,7 @@ type DataPropertyProps = {
fieldSpec?: object
value?: DataPropertyValue
errors?: object
};
} & WithTranslation;
type DataPropertyState = {
refs: {[key: number]: string}
@@ -65,7 +66,7 @@ export type Stop = [{
value: number
}, number]
export default class DataProperty extends React.Component<DataPropertyProps, DataPropertyState> {
class DataPropertyInternal extends React.Component<DataPropertyInternalProps, DataPropertyState> {
state = {
refs: {} as {[key: number]: string}
}
@@ -80,7 +81,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
}
}
static getDerivedStateFromProps(props: DataPropertyProps, state: DataPropertyState) {
static getDerivedStateFromProps(props: Readonly<DataPropertyInternalProps>, state: DataPropertyState) {
const newRefs = setStopRefs(props, state);
if(newRefs) {
return {
@@ -213,6 +214,8 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
}
render() {
const t = this.props.t;
if (typeof this.props.value?.type === "undefined") {
this.props.value!.type = this.getFieldFunctionType(this.props.fieldSpec)
}
@@ -227,8 +230,8 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
const dataProps = {
'aria-label': "Input value",
label: "Data value",
'aria-label': t("Input value"),
label: t("Data value"),
value: dataLevel as any,
onChange: (newData: string | number | undefined) => this.changeStop(idx, { zoom: zoomLevel, value: newData as number }, value)
}
@@ -263,7 +266,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
</td>
<td>
<InputSpec
aria-label="Output value"
aria-label={t("Output value")}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec}
value={value}
@@ -282,21 +285,21 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
<div className="maputnik-data-fieldset-inner">
<Block
label={"Function"}
label={t("Function")}
key="function"
>
<div className="maputnik-data-spec-property-input">
<InputSelect
value={this.props.value!.type}
onChange={(propVal: string) => this.changeDataType(propVal)}
title={"Select a type of data scale (default is 'categorical')."}
title={t("Select a type of data scale (default is 'categorical').")}
options={this.getDataFunctionTypes(this.props.fieldSpec)}
/>
</div>
</Block>
{this.props.value?.type !== "identity" &&
<Block
label={"Base"}
label={t("Base")}
key="base"
>
<div className="maputnik-data-spec-property-input">
@@ -316,14 +319,14 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
<div className="maputnik-data-spec-property-input">
<InputString
value={this.props.value?.property}
title={"Input a data property to base styles off of."}
title={t("Input a data property to base styles off of.")}
onChange={propVal => this.changeDataProperty("property", propVal)}
/>
</div>
</Block>
{dataFields &&
<Block
label={"Default"}
label={t("Default")}
key="default"
>
<InputSpec
@@ -337,12 +340,12 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
{dataFields &&
<div className="maputnik-function-stop">
<table className="maputnik-function-stop-table">
<caption>Stops</caption>
<caption>{t("Stops")}</caption>
<thead>
<tr>
<th>Zoom</th>
<th>Input value</th>
<th rowSpan={2}>Output value</th>
<th>{t("Zoom")}</th>
<th>{t("Input value")}</th>
<th rowSpan={2}>{t("Output value")}</th>
</tr>
</thead>
<tbody>
@@ -359,7 +362,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add stop
</svg> {t("Add stop")}
</InputButton>
}
<InputButton
@@ -368,7 +371,7 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg> Convert to expression
</svg> {t("Convert to expression")}
</InputButton>
</div>
</div>
@@ -376,3 +379,6 @@ export default class DataProperty extends React.Component<DataPropertyProps, Dat
</div>
}
}
const DataProperty = withTranslation()(DataPropertyInternal);
export default DataProperty;

View File

@@ -2,21 +2,26 @@ import React from 'react'
import InputButton from './InputButton'
import {MdDelete} from 'react-icons/md'
import { WithTranslation, withTranslation } from 'react-i18next';
type DeleteStopButtonProps = {
type DeleteStopButtonInternalProps = {
onClick?(...args: unknown[]): unknown
};
} & WithTranslation;
export default class DeleteStopButton extends React.Component<DeleteStopButtonProps> {
class DeleteStopButtonInternal extends React.Component<DeleteStopButtonInternalProps> {
render() {
const t = this.props.t;
return <InputButton
className="maputnik-delete-stop"
onClick={this.props.onClick}
title={"Remove zoom level from stop"}
title={t("Remove zoom level from stop")}
>
<MdDelete />
</InputButton>
}
}
const DeleteStopButton = withTranslation()(DeleteStopButtonInternal);
export default DeleteStopButton;

View File

@@ -1,6 +1,7 @@
import React from 'react'
import {MdDelete, MdUndo} from 'react-icons/md'
import stringifyPretty from 'json-stringify-pretty-compact'
import { WithTranslation, withTranslation } from 'react-i18next';
import Block from './Block'
import InputButton from './InputButton'
@@ -8,7 +9,7 @@ import labelFromFieldName from '../libs/label-from-field-name'
import FieldJson from './FieldJson'
type ExpressionPropertyProps = {
type ExpressionPropertyInternalProps = {
onDelete?(...args: unknown[]): unknown
fieldName: string
fieldType?: string
@@ -20,20 +21,20 @@ type ExpressionPropertyProps = {
canUndo?(...args: unknown[]): unknown
onFocus?(...args: unknown[]): unknown
onBlur?(...args: unknown[]): unknown
};
} & WithTranslation;
type ExpressionPropertyState = {
jsonError: boolean
};
export default class ExpressionProperty extends React.Component<ExpressionPropertyProps, ExpressionPropertyState> {
class ExpressionPropertyInternal extends React.Component<ExpressionPropertyInternalProps, ExpressionPropertyState> {
static defaultProps = {
errors: {},
onFocus: () => {},
onBlur: () => {},
}
constructor (props:ExpressionPropertyProps) {
constructor(props: ExpressionPropertyInternalProps) {
super(props);
this.state = {
jsonError: false,
@@ -53,7 +54,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
}
render() {
const {errors, fieldName, fieldType, value, canUndo} = this.props;
const {t, errors, fieldName, fieldType, value, canUndo} = this.props;
const {jsonError} = this.state;
const undoDisabled = canUndo ? !canUndo() : true;
@@ -65,7 +66,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
onClick={this.props.onUndo}
disabled={undoDisabled}
className="maputnik-delete-stop"
title="Revert from expression"
title={t("Revert from expression")}
>
<MdUndo />
</InputButton>
@@ -74,7 +75,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
key="delete_action"
onClick={this.props.onDelete}
className="maputnik-delete-stop"
title="Delete expression"
title={t("Delete expression")}
>
<MdDelete />
</InputButton>
@@ -112,7 +113,7 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
// this feels like an incorrect type...? `foundErrors` is an array of objects, not a single object
error={foundErrors as any}
fieldSpec={this.props.fieldSpec}
label={labelFromFieldName(this.props.fieldName)}
label={t(labelFromFieldName(this.props.fieldName))}
action={deleteStopBtn}
wideMode={true}
>
@@ -137,3 +138,6 @@ export default class ExpressionProperty extends React.Component<ExpressionProper
</Block>
}
}
const ExpressionProperty = withTranslation()(ExpressionPropertyInternal);
export default ExpressionProperty;

View File

@@ -3,16 +3,18 @@ import React from 'react'
import InputButton from './InputButton'
import {MdFunctions, MdInsertChart} from 'react-icons/md'
import {mdiFunctionVariant} from '@mdi/js';
import { WithTranslation, withTranslation } from 'react-i18next';
type FunctionInputButtonsProps = {
type FunctionInputButtonsInternalProps = {
fieldSpec?: any
onZoomClick?(...args: unknown[]): unknown
onDataClick?(...args: unknown[]): unknown
onExpressionClick?(...args: unknown[]): unknown
};
} & WithTranslation;
export default class FunctionInputButtons extends React.Component<FunctionInputButtonsProps> {
class FunctionInputButtonsInternal extends React.Component<FunctionInputButtonsInternalProps> {
render() {
const t = this.props.t;
let makeZoomInputButton, makeDataInputButton, expressionInputButton;
if (this.props.fieldSpec.expression.parameters.includes('zoom')) {
@@ -20,7 +22,7 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
<InputButton
className="maputnik-make-zoom-function"
onClick={this.props.onExpressionClick}
title="Convert to expression"
title={t("Convert to expression")}
>
<svg style={{width:"14px", height:"14px", verticalAlign: "middle"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
@@ -31,7 +33,7 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
makeZoomInputButton = <InputButton
className="maputnik-make-zoom-function"
onClick={this.props.onZoomClick}
title="Convert property into a zoom function"
title={t("Convert property into a zoom function")}
>
<MdFunctions />
</InputButton>
@@ -40,7 +42,7 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
makeDataInputButton = <InputButton
className="maputnik-make-data-function"
onClick={this.props.onDataClick}
title="Convert property to data function"
title={t("Convert property to data function")}
>
<MdInsertChart />
</InputButton>
@@ -56,3 +58,6 @@ export default class FunctionInputButtons extends React.Component<FunctionInputB
}
}
}
const FunctionInputButtons = withTranslation()(FunctionInputButtonsInternal);
export default FunctionInputButtons;

View File

@@ -1,6 +1,7 @@
import React from 'react'
import {mdiFunctionVariant, mdiTableRowPlusAfter} from '@mdi/js';
import latest from '@maplibre/maplibre-gl-style-spec/dist/latest.json'
import { WithTranslation, withTranslation } from 'react-i18next';
import InputButton from './InputButton'
import InputSpec from './InputSpec'
@@ -20,7 +21,7 @@ import sortNumerically from '../libs/sort-numerically'
*
* When the stops are reordered the references are also updated (see this.orderStops) this allows React to use the same key for the element and keep keyboard focus.
*/
function setStopRefs(props: ZoomPropertyProps, state: ZoomPropertyState) {
function setStopRefs(props: ZoomPropertyInternalProps, state: ZoomPropertyState) {
// This is initialsed below only if required to improved performance.
let newRefs: {[key: number]: string} = {};
@@ -45,7 +46,7 @@ type ZoomWithStops = {
}
type ZoomPropertyProps = {
type ZoomPropertyInternalProps = {
onChange?(...args: unknown[]): unknown
onChangeToDataFunction?(...args: unknown[]): unknown
onDeleteStop?(...args: unknown[]): unknown
@@ -59,13 +60,13 @@ type ZoomPropertyProps = {
}
errors?: object
value?: ZoomWithStops
};
} & WithTranslation;
type ZoomPropertyState = {
refs: {[key: number]: string}
}
export default class ZoomProperty extends React.Component<ZoomPropertyProps, ZoomPropertyState> {
class ZoomPropertyInternal extends React.Component<ZoomPropertyInternalProps, ZoomPropertyState> {
static defaultProps = {
errors: {},
}
@@ -84,7 +85,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
}
}
static getDerivedStateFromProps(props: ZoomPropertyProps, state: ZoomPropertyState) {
static getDerivedStateFromProps(props: Readonly<ZoomPropertyInternalProps>, state: ZoomPropertyState) {
const newRefs = setStopRefs(props, state);
if(newRefs) {
return {
@@ -152,17 +153,17 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
}
render() {
const t = this.props.t;
const zoomFields = this.props.value?.stops.map((stop, idx) => {
const zoomLevel = stop[0]
const key = this.state.refs[idx];
const value = stop[1]
const deleteStopBtn= <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
const deleteStopBtn = <DeleteStopButton onClick={this.props.onDeleteStop?.bind(this, idx)} />
return <tr
key={key}
key={`${stop[0]}-${stop[1]}`}
>
<td>
<InputNumber
aria-label="Zoom"
aria-label={t("Zoom")}
value={zoomLevel}
onChange={changedStop => this.changeZoomStop(idx, changedStop, value)}
min={0}
@@ -171,7 +172,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
</td>
<td>
<InputSpec
aria-label="Output value"
aria-label={t("Output value")}
fieldName={this.props.fieldName}
fieldSpec={this.props.fieldSpec as any}
value={value}
@@ -190,19 +191,19 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
<legend>{labelFromFieldName(this.props.fieldName)}</legend>
<div className="maputnik-data-fieldset-inner">
<Block
label={"Function"}
label={t("Function")}
>
<div className="maputnik-data-spec-property-input">
<InputSelect
value={"interpolate"}
onChange={(propVal: string) => this.changeDataType(propVal)}
title={"Select a type of data scale (default is 'categorical')."}
title={t("Select a type of data scale (default is 'categorical').")}
options={this.getDataFunctionTypes(this.props.fieldSpec!)}
/>
</div>
</Block>
<Block
label={"Base"}
label={t("Base")}
>
<div className="maputnik-data-spec-property-input">
<InputSpec
@@ -215,11 +216,11 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
</Block>
<div className="maputnik-function-stop">
<table className="maputnik-function-stop-table maputnik-function-stop-table--zoom">
<caption>Stops</caption>
<caption>{t("Stops")}</caption>
<thead>
<tr>
<th>Zoom</th>
<th rowSpan={2}>Output value</th>
<th>{t("Zoom")}</th>
<th rowSpan={2}>{t("Output value")}</th>
</tr>
</thead>
<tbody>
@@ -234,7 +235,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiTableRowPlusAfter} />
</svg> Add stop
</svg> {t("Add stop")}
</InputButton>
<InputButton
className="maputnik-add-stop"
@@ -242,7 +243,7 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
>
<svg style={{width:"14px", height:"14px", verticalAlign: "text-bottom"}} viewBox="0 0 24 24">
<path fill="currentColor" d={mdiFunctionVariant} />
</svg> Convert to expression
</svg> {t("Convert to expression")}
</InputButton>
</div>
</div>
@@ -262,3 +263,6 @@ export default class ZoomProperty extends React.Component<ZoomPropertyProps, Zoo
}
}
}
const ZoomProperty = withTranslation()(ZoomPropertyInternal);
export default ZoomProperty;

40
src/i18n.ts Normal file
View File

@@ -0,0 +1,40 @@
import i18n from "i18next";
import detector from "i18next-browser-languagedetector";
import resourcesToBackend from "i18next-resources-to-backend";
import { initReactI18next } from "react-i18next";
export const supportedLanguages = {
"en": "English",
"ja": "日本語",
"he": "עברית",
"zh": "简体中文"
} as const;
i18n
.use(detector) // detect user language from browser settings
.use(
resourcesToBackend((lang: string, ns: string) => {
if (lang === "en") {
// English is the default language, so we don't need to load any resources for it.
return {};
}
return import(`./locales/${lang}/${ns}.json`);
})
)
.use(initReactI18next) // required to initialize react-i18next
.init({
supportedLngs: Object.keys(supportedLanguages),
keySeparator: false, // we do not use keys in form messages.welcome
nsSeparator: false,
interpolation: {
escapeValue: false // React already escapes for us
},
saveMissing: true, // this needs to be set for missingKeyHandler to work
fallbackLng: false, // we set the fallback to false so we can get the correct language in the missingKeyHandler
missingKeyHandler: (lngs, _ns, key) => {
if (lngs[0] === "en") { return; }
console.warn(`Missing translation for "${key}" in "${lngs.join(", ")}"`);
}
});
export default i18n;

View File

@@ -3,6 +3,7 @@ import { createRoot } from 'react-dom/client';
import './favicon.ico'
import './styles/index.scss'
import './i18n';
import App from './components/App';
const root = createRoot(document.querySelector("#app"));

View File

@@ -1,18 +1,20 @@
const spec = {
import { TFunction } from "i18next";
const spec = (t: TFunction) => ({
maputnik: {
maptiler_access_token: {
label: "MapTiler Access Token",
doc: "Public access token for MapTiler Cloud."
label: t("MapTiler Access Token"),
doc: t("Public access token for MapTiler Cloud.")
},
thunderforest_access_token: {
label: "Thunderforest Access Token",
doc: "Public access token for Thunderforest services."
label: t("Thunderforest Access Token"),
doc: t("Public access token for Thunderforest services.")
},
style_renderer: {
label: "Style Renderer",
doc: "Choose the default Maputnik renderer for this style.",
label: t("Style Renderer"),
doc: t("Choose the default Maputnik renderer for this style."),
},
}
}
})
export default spec;

View File

@@ -5,13 +5,15 @@ export default class ZoomControl {
_container: HTMLDivElement | undefined = undefined;
_textEl: HTMLSpanElement | null = null;
constructor(public label: string) {}
onAdd(map: Map) {
this._map = map;
this._container = document.createElement('div');
this._container.className = 'maplibregl-ctrl maplibregl-ctrl-group maplibregl-ctrl-zoom';
this._container.setAttribute("data-wd-key", "maplibre:ctrl-zoom");
this._container.innerHTML = `
Zoom: <span></span>
${this.label} <span></span>
`;
this._textEl = this._container.querySelector("span");

View File

@@ -0,0 +1,184 @@
{
"Input value": "ערך קבלה",
"Data value": "ערך מידע",
"Output value": "ערך החזרה",
"Function": "פונקציה",
"Select a type of data scale (default is 'categorical').": "בחרו סוג מידע לטובת הגדלה (ברירת המחדל היא קטגוריה)",
"Base": "בסיס",
"Input a data property to base styles off of.": "הכניסו מידע עליו יש לבסס את הסטייל",
"Default": "ברירת המחדל",
"Stops": "עצירות",
"Zoom": "זום",
"Add stop": "הוספת עצירה",
"Convert to expression": "המחרה לביטוי",
"Remove zoom level from stop": "הסרת רמת זום מעצירה",
"Revert from expression": "החזרה מביטוי",
"Delete expression": "מחיקת ביטוי",
"Convert property into a zoom function": "המרה לפונקציית זום",
"Convert property to data function": "המרה לפונקציית מידע",
"Layer <1>{formatLayerId(layerId)}</1>: {parsed.data.message}": "שכבה <1>{formatLayerId(layerId)}</1>: {parsed.data.message}",
"switch to layer": "שינוי לשכבה",
"Map": "מפה",
"Inspect": "בדיקה",
"Deuteranopia filter": "Deuteranopia filter",
"Protanopia filter": "Protanopia filter",
"Tritanopia filter": "Tritanopia filter",
"Achromatopsia filter": "Achromatopsia filter",
"Layers list": "רשימת שכבות",
"Layer editor": "עורך השכבות",
"Map view": "תצוגת מפה",
"Maputnik on GitHub": "מפוטניק בגיטהב",
"Open": "פתיחה",
"Export": "ייצוא",
"Data Sources": "מקורות מידע",
"Style Settings": "הגדרות הסטייל",
"View": "תצוגה",
"Color accessibility": "נגישות צבעים",
"Language": "שפה",
"Help": "עזרה",
"Comments for the current layer. This is non-standard and not in the spec.": "הערות על השכבה הנוכחית. זה לא חלק מהספסיפיקציות",
"Comments": "הערות",
"Comment...": "הערה...",
"Max Zoom": "זום מקסימאלי",
"Min Zoom": "זום מינימאלי",
"Source": "מקור",
"Source Layer": "שכבת מקור",
"Type": "סוג",
"Nested filters are not supported.": "סינונים מקוננים אינם נתמכים",
"Upgrade to expression": "שדרוג לביטוי",
"Filter": "סינון",
"every filter matches": "כל הסינונים מתאימים",
"no filter matches": "אף סינון אינו מתאים",
"any filter matches": "אחד הסינוים מתאימים",
"Add filter": "הוספת סינון",
"You've entered an old style filter.": "הכנסתם סינון מסוג ישן,",
"Switch to filter editor.": "עברו לעורך הסינונים",
"Delete filter block": "מחיקת גוש מסנן",
"Add value": "הוספת ערך",
"Remove array item": "הסרת איבר מערך",
"Press <1>ESC</1> to lose focus": "לחצו על <1>ESC</1> על מנת לאבד פוקוס",
"Must provide protocol: <1>https://</1>": "נדרש פרוטוקול: <1>https://</1>",
"Must provide protocol: <1>http://</1> or <3>https://</3>": "נדרשים פרוטוקולים: <1>http://</1> או <3>https://</3>",
"CORS policy won't allow fetching resources served over http from https, use a <1>https://</1> domain": "CORS policy won't allow fetching resources served over http from https, use a <1>https://</1> domain",
"Layer": "שכבה",
"JSON Editor": "עורך JSON",
"Delete": "מחיקה",
"Duplicate": "שכפול",
"Show": "הצגה",
"Hide": "הסתרה",
"Move layer up": "העלה שכבה",
"Move layer down": "הורד שכבה",
"Layer: {{layerId}}": "שכבה: {{layerId}}",
"Layers": "שכבות",
"Collapse": "הקטנה",
"Expand": "הגדלה",
"Add Layer": "הוספת שכבה",
"Zoom:": "זום:",
"Search": "חיפוש",
"Close popup": "סגירת החלון",
"cursor:": "סמן",
"center:": "מרכז:",
"rotation:": "סיבוב:",
"Close modal": "סגירת חלונית",
"Debug": "דיבאג",
"Options": "אפשרויות",
"<0>Open in OSM</0> &mdash; Opens the current view on openstreetmap.org": "<0>פתיחה ב-OSM</0> - פתיחה של התצוגה הנוכחית ב- openstreetmap.org",
"Export Style": "ייצוא של הסטייל",
"Download Style": "הורדה של הסטייל",
"Download a JSON style to your computer.": "הורדה של הסטייל למחשב",
"Download HTML": "הורדה כ-HTML",
"Cancel": "ביטול",
"Open Style": "פתיחת סטייל",
"Upload Style": "העלאה של סטייל",
"Upload a JSON style from your computer.": "העלאה של סטייל מהמחשב",
"Style file": "קובץ סטייל",
"Upload": "העלאה",
"Load from URL": "פתיחה מתוך כתובת",
"Load from a URL. Note that the URL must have <1>CORS enabled</1>.": "פתיחה מכתובת, שימו לב: הכתובת צריכה לתמוך ב- CORS",
"Style URL": "כתוסת סטייל",
"Enter URL...": "הכנסו כתובת",
"Gallery Styles": "גלריית סטיילים",
"Open one of the publicly available styles to start from.": "פתיחת אחד הסטייליםפ הציבוריים על מנת להתחיל מהם.",
"Loading style": "טוען סטייל",
"Loading: {{requestUrl}}": "טוען: {{requestUrl}}",
"Name": "שם",
"Owner": "שייך ל",
"Owner ID of the style. Used by Mapbox or future style APIs.": "מזהה השייכות של הסטייל.",
"Sprite URL": "כתובת ספרייט",
"Glyphs URL": "כתובת סמלילים",
"Center": "מרכז",
"Bearing": "כיוון",
"Pitch": "זווית",
"Light anchor": "עוגן אור",
"Light color": "צבע האור",
"Light intensity": "חוזק האור",
"Light position": "מיקום האור",
"Terrain source": "מקור terrain",
"Terrain exaggeration": "כמה \"להגזים\"",
"Transition delay": "זמן לפני מעבר",
"Transition duration": "זמן מעבר",
"Open Layers (experimental)": "Open Layers (experimental)",
"Shortcuts menu": "תפריט קיצורי דרך",
"Open modal": "פתיחת חלונית",
"Export modal": "חלונית ייצוא",
"Data Sources modal": "חלונית מקורות",
"Style Settings modal": "חלונית הגדרות סטייל",
"Toggle inspect": "שינוי בדיקה",
"Focus map": "פיקוס המפה",
"Debug modal": "חלונית דיבאג",
"Increase the zoom level by 1.": "העלאה של הזום ב-1",
"Increase the zoom level by 2.": "העלאה של הזום ב-2",
"Decrease the zoom level by 1.": "הורדה של הזום ב-1",
"Decrease the zoom level by 2.": "הורדה של הזום ב-2",
"Pan up by 100 pixels.": "תזוזה למעלה ב-100 פיקסלים",
"Pan down by 100 pixels.": "תזוזה למטה ב-100 פיקסלים",
"Pan left by 100 pixels.": "תזוזה שמאלה ב-100 פיקסלים",
"Pan right by 100 pixels.": "תזוזה ימינה ב-100 פיקסלים",
"Increase the rotation by 15 degrees.": "הוספת 15 מעלות לסיבוב",
"Decrease the rotation by 15 degrees.": "הורדת 15 מעלות לסיבוב",
"Increase the pitch by 10 degrees.": "הוספה של 10 מעלות לזיווית",
"Decrease the pitch by 10 degrees.": "הורדה של 10 מעלות מהזווית",
"Shortcuts": "קיצורי דרך",
"Press <1>ESC</1> to lose focus of any active elements, then press one of:": "לחיצה על <1>ESC</1> על מנת לאבד פוקוס מכל פקד פעיל, ואז ליחצו על אחד מאלו:",
"If the Map is in focused you can use the following shortcuts": "אם המפה נמצאת בפוקוס תוכלו להשתמש בקיצורי הדרך",
"Remove '{{sourceId}}' source": "הורדה של מקור '{{sourceId}}'",
"Source ID": "מזהה מקור",
"Unique ID that identifies the source and is used in the layer to reference the source.": "מזהה מקור ייחודי לטובת שימוש בשכבה",
"Source Type": "סוג מקור",
"GeoJSON (JSON)": "GeoJSON (JSON)",
"GeoJSON (URL)": "GeoJSON (URL)",
"Vector (TileJSON URL)": "Vector (TileJSON URL)",
"Vector (XYZ URLs)": "Vector (XYZ URLs)",
"Raster (TileJSON URL)": "Raster (TileJSON URL)",
"Raster (XYZ URL)": "Raster (XYZ URL)",
"Raster DEM (TileJSON URL)": "Raster DEM (TileJSON URL)",
"Raster DEM (XYZ URLs)": "Raster DEM (XYZ URLs)",
"Image": "תמונה",
"Video": "וידאו",
"Add Source": "הוספת מקור",
"Sources": "מקורות",
"Active Sources": "מקורות פעילים",
"Choose Public Source": "בחירת מקור ציבורי",
"Add one of the publicly available sources to your style.": "הוספת אחד המקורות הציבוריים הזמינים לסטייל שלך.",
"Add New Source": "הוספת מקור חדש",
"Add a new source to your style. You can only choose the source type and id at creation time!": "הוספת מקור לסטייל, ביכולתכם לעשות זאת רק בזמן מסויים",
"TileJSON URL": "כתובת TileJSON",
"Tile URL": "כתובת אריחים",
"Coord top left": "מיקום שמאלי עליון",
"Coord top right": "מיקום ימני עליון",
"Coord bottom right": "מיקום ימני תחתון",
"Coord bottom left": "מיקום שמאלי תחתון",
"Image URL": "כתובת תמונה",
"Video URL": "כתובת וידאו",
"GeoJSON URL": "כתובת GeoJSON",
"GeoJSON": "GeoJSON",
"Cluster": "קיבוץ",
"Encoding": "קידוד",
"Error:": "שגיאה",
"MapTiler Access Token": "MapTiler Access Token",
"Public access token for MapTiler Cloud.": "Public access token for MapTiler Cloud.",
"Thunderforest Access Token": "Thunderforest Access Token",
"Public access token for Thunderforest services.": "Public access token for Thunderforest services.",
"Style Renderer": "צייר הסטייל",
"Choose the default Maputnik renderer for this style.": "בחירת צייר ברירת המחדל של מפוטניק עבור הסטייל הזה"
}

View File

@@ -0,0 +1,192 @@
{
"Input value": "入力値",
"Data value": "データ値",
"Output value": "値",
"Function": "関数",
"Select a type of data scale (default is 'categorical').": "データスケールの種類を選択してくださいデフォルトは「categorical」です。",
"Base": "ベース",
"Input a data property to base styles off of.": "スタイルのベースとなるデータプロパティを入力してください。",
"Default": "デフォルト",
"Stops": "ストップ",
"Zoom": "ズーム",
"Add stop": "ストップを追加",
"Convert to expression": "フィルタ式に変換",
"Remove zoom level from stop": "ズームレベルをストップから削除",
"Revert from expression": "式から戻す",
"Delete expression": "式を削除",
"Convert property into a zoom function": "プロパティをズーム関数に変換する",
"Convert property to data function": "プロパティをデータ関数に変換する",
"Layer <1>{formatLayerId(layerId)}</1>: {parsed.data.message}": "レイヤ<1>{formatLayerId(layerId)}</1>: {parsed.data.message}",
"switch to layer": "レイヤへ切替",
"Map": "地図",
"Inspect": "検査",
"Deuteranopia filter": "緑色盲フィルタ",
"Protanopia filter": "赤色盲フィルタ",
"Tritanopia filter": "青色盲フィルタ",
"Achromatopsia filter": "全色盲フィルタ",
"Layers list": "レイヤー一覧",
"Layer editor": "レイヤーエディタ",
"Map view": "地図画面",
"Maputnik on GitHub": "GitHubのMaputnik",
"Open": "開く",
"Export": "エクスポート",
"Data Sources": "データソース",
"Style Settings": "スタイル設定",
"View": "表示",
"Color accessibility": "色のアクセシビリティ",
"Language": "言語",
"Help": "ヘルプ",
"Comments for the current layer. This is non-standard and not in the spec.": "現在のレイヤーのコメント。注意:この機能は標準ではないため、他のライブラリとの互換性状況はわかりません。",
"Comments": "コメント",
"Comment...": "コメントを書く",
"Max Zoom": "最大ズーム",
"Min Zoom": "最小ズーム",
"Source": "ソース",
"Source Layer": "ソースレイヤ",
"Type": "タイプ",
"Nested filters are not supported.": "ネストされたフィルタはサポートされていません。",
"Upgrade to expression": "式にアップグレード",
"Filter": "フィルタ",
"every filter matches": "全てのフィルタが一致",
"no filter matches": "フィルタが一致しない",
"any filter matches": "いずれかのフィルタが一致",
"Add filter": "フィルタを追加",
"You've entered an old style filter.": "旧型フィルタを使用しております。",
"Switch to filter editor.": "新型に変換",
"Delete filter block": "フィルタブロックを削除",
"Add value": "値を追加する",
"Remove array item": "配列の項目を削除",
"Press <1>ESC</1> to lose focus": "<1>ESC</1>を押すとフォーカスが外れます",
"Must provide protocol: <1>https://</1>": "プロトコルを指定してください: <1>https://</1>",
"Must provide protocol: <1>http://</1> or <3>https://</3>": "プロトコルを指定してください: <1>http://</1> または <3>https://</3>",
"CORS policy won't allow fetching resources served over http from https, use a <1>https://</1> domain": "CORS使用時は、http経由で提供されるリソースをhttpsから取得することはできません。<1>https://</1> ドメインを使用してください。",
"Layer": "レイヤー",
"JSON Editor": "JSONエディタ",
"Delete": "削除",
"Duplicate": "複製",
"Show": "表示",
"Hide": "非表示",
"Move layer up": "上に移動",
"Move layer down": "下に移動",
"Layer: {{layerId}}": "レイヤー {{layerId}}",
"Layers": "レイヤー",
"Collapse": "畳む",
"Expand": "展開",
"Add Layer": "レイヤー追加",
"Zoom:": "ズーム:",
"Search": "検索",
"Close popup": "ポップアップを閉じる",
"cursor:": "カーソル",
"center:": "中央:",
"rotation:": "回転角度:",
"Close modal": "モーダルを閉じる",
"Debug": "デバッグ",
"Options": "設定",
"<0>Open in OSM</0> &mdash; Opens the current view on openstreetmap.org": "現在のビューを <0>openstreetmap.org で開く</0>",
"Export Style": "スタイルをエクスポート",
"Download Style": "スタイルをダウンロード",
"Download a JSON style to your computer.": "パソコンにJSONスタイルをダウンロードします。",
"Download HTML": "HTMLをダウンロード",
"Cancel": "キャンセル",
"Open Style": "スタイルを開く",
"Upload Style": "スタイルをアップロードする",
"Upload a JSON style from your computer.": "JSONスタイルをパソコンからアップロードする",
"Style file": "スタイルファイル",
"Upload": "アップロード",
"Load from URL": "URLから読み込む",
"Load from a URL. Note that the URL must have <1>CORS enabled</1>.": "URLから読み込む。注意: URLは <1>CORSを有効にする</1> 必要があります。",
"Style URL": "スタイルURL",
"Enter URL...": "URLを入力",
"Gallery Styles": "ギャラリースタイル",
"Open one of the publicly available styles to start from.": "公開スタイルを選んで開始しましょう。",
"Loading style": "スタイル読込中",
"Loading: {{requestUrl}}": "{{requestUrl}} を読み込み中",
"Name": "名前",
"Owner": "所有者",
"Owner ID of the style. Used by Mapbox or future style APIs.": "スタイルの所有者 ID。Mapbox または将来のスタイル API によって使用されます。",
"Sprite URL": "スプライトURL",
"Glyphs URL": "フォントグリフURL",
"Center": "中央",
"Bearing": "方位",
"Pitch": "ピッチ",
"Light anchor": "ライトアンカー",
"Light color": "ライトカラー",
"Light intensity": "ライト強度",
"Light position": "ライト位置",
"Terrain source": "地形ソース",
"Terrain exaggeration": "地形の誇張",
"Transition delay": "遷移遅延",
"Transition duration": "遷移期間",
"Open Layers (experimental)": "Open Layers (実験的)",
"Shortcuts menu": "ショートカットメニュー",
"Open modal": "モーダルを開く",
"Export modal": "書き出しのモーダル",
"Data Sources modal": "データソースのモーダル",
"Style Settings modal": "スタイル設定のモーダル",
"Toggle inspect": "検査の切り替え",
"Focus map": "地図にフォーカス",
"Debug modal": "デバッグモーダル",
"Increase the zoom level by 1.": "ズームレベルを1増やす",
"Increase the zoom level by 2.": "ズームレベルを2増やす",
"Decrease the zoom level by 1.": "ズームレベルを1減らす",
"Decrease the zoom level by 2.": "ズームレベルを2減らす",
"Pan up by 100 pixels.": "100ピクセル上に移動",
"Pan down by 100 pixels.": "100ピクセル下に移動",
"Pan left by 100 pixels.": "100ピクセル左に移動",
"Pan right by 100 pixels.": "100ピクセル右に移動",
"Increase the rotation by 15 degrees.": "回転角度を15度増やす",
"Decrease the rotation by 15 degrees.": "回転角度を15度減らす",
"Increase the pitch by 10 degrees.": "ピッチを10度増やす",
"Decrease the pitch by 10 degrees.": "ピッチを10度減らす",
"Shortcuts": "ショートカット",
"Press <1>ESC</1> to lose focus of any active elements, then press one of:": "<1>ESC</1> を押してアクティブな要素からフォーカスを外し、次に以下のいずれかを押してください:",
"If the Map is in focused you can use the following shortcuts": "地図がフォーカスされている場合、以下のショートカットを使用できます",
"Remove '{{sourceId}}' source": "'{{sourceId}}' ソースを削除",
"Source ID": "ソースID",
"Unique ID that identifies the source and is used in the layer to reference the source.": "ソースを識別するためのユニークID。レイヤーでソースを参照するために使用されます。",
"Source Type": "ソースタイプ",
"GeoJSON (JSON)": "GeoJSON (JSON)",
"GeoJSON (URL)": "GeoJSON (URL)",
"Vector (TileJSON URL)": "ベクトル (TileJSON URL)",
"Vector (XYZ URLs)": "ベクトル (XYZ URL)",
"Raster (TileJSON URL)": "ラスタ (TileJSON URL)",
"Raster (XYZ URL)": "ラスタ (XYZ URL)",
"Raster DEM (TileJSON URL)": "ラスタ DEM (TileJSON URL)",
"Raster DEM (XYZ URLs)": "ラスタ DEM (XYZ URL)",
"Image": "画像",
"Video": "動画",
"Add Source": "ソースを追加",
"Sources": "ソース一覧",
"Active Sources": "使用中ソース",
"Choose Public Source": "公開ソースから選択",
"Add one of the publicly available sources to your style.": "公開ソースをスタイルに追加しましょう",
"Add New Source": "新規ソースを追加",
"Add a new source to your style. You can only choose the source type and id at creation time!": "スタイルに新規ソースを追加します。注意: 作成時にソースタイプとIDのみを選択できます。",
"TileJSON URL": "TileJSON URL",
"Tile URL": "タイルURL",
"Coord top left": "左上座標",
"Coord top right": "右上座標",
"Coord bottom right": "右下座標",
"Coord bottom left": "左下座標",
"Image URL": "画像URL",
"Video URL": "動画URL",
"GeoJSON URL": "GeoJSON URL",
"GeoJSON": "GeoJSON",
"Cluster": "クラスタ",
"Encoding": "エンコーディング",
"Error:": "エラー:",
"MapTiler Access Token": "MapTiler アクセストークン",
"Public access token for MapTiler Cloud.": "MapTiler Cloud の公開用アクセストークン",
"Thunderforest Access Token": "Thunderforest アクセストークン",
"Public access token for Thunderforest services.": "Thunderforest サービスの公開用アクセストークン",
"Style Renderer": "スタイルレンダラ",
"Choose the default Maputnik renderer for this style.": "このスタイルのデフォルトの Maputnik レンダラを選択してください",
"Layer options": "レイヤー設定",
"Paint properties": "ペイントプロパティ",
"Layout properties": "レイアウトプロパティ",
"General layout properties": "一般レイアウトプロパティ",
"Text layout properties": "文字レイアウトプロパティ",
"Icon layout properties": "アイコンレイアウトプロパティ",
"Text paint properties": "文字ペイントプロパティ",
"Icon paint properties": "アイコンペイントプロパティ"
}

View File

@@ -0,0 +1,192 @@
{
"Input value": "输入值",
"Data value": "数据值",
"Output value": "输出值",
"Function": "函数",
"Select a type of data scale (default is 'categorical').": "选择数据尺度的类型(默认为“分类”)。",
"Base": "基础",
"Input a data property to base styles off of.": "输入一个数据属性作为样式的基础。",
"Default": "默认",
"Stops": "停靠点",
"Zoom": "缩放",
"Add stop": "添加停靠点",
"Convert to expression": "转换为表达式",
"Remove zoom level from stop": "从停靠点移除缩放级别",
"Revert from expression": "从表达式恢复",
"Delete expression": "删除表达式",
"Convert property into a zoom function": "将属性转换为缩放函数",
"Convert property to data function": "将属性转换为数据函数",
"Layer <1>{formatLayerId(layerId)}</1>: {parsed.data.message}": "图层<1>{formatLayerId(layerId)}</1>: {parsed.data.message}",
"switch to layer": "切换到图层",
"Map": "地图",
"Inspect": "检查",
"Deuteranopia filter": "绿色盲滤镜",
"Protanopia filter": "红色盲滤镜",
"Tritanopia filter": "蓝色盲滤镜",
"Achromatopsia filter": "全色盲滤镜",
"Layers list": "图层列表",
"Layer editor": "图层编辑器",
"Map view": "地图视图",
"Maputnik on GitHub": "GitHub上的Maputnik",
"Open": "打开",
"Export": "导出",
"Data Sources": "数据源",
"Style Settings": "样式设置",
"View": "视图",
"Color accessibility": "颜色可访问性",
"Language": "语言",
"Help": "帮助",
"Comments for the current layer. This is non-standard and not in the spec.": "当前图层的注释。注意:这不是标准功能,可能与其他库不兼容。",
"Comments": "注释",
"Comment...": "写注释...",
"Max Zoom": "最大缩放",
"Min Zoom": "最小缩放",
"Source": "源",
"Source Layer": "源图层",
"Type": "类型",
"Nested filters are not supported.": "不支持嵌套过滤器。",
"Upgrade to expression": "升级为表达式",
"Filter": "过滤器",
"every filter matches": "每个过滤器都匹配",
"no filter matches": "没有过滤器匹配",
"any filter matches": "任何过滤器匹配",
"Add filter": "添加过滤器",
"You've entered an old style filter.": "您输入了一个旧风格的过滤器。",
"Switch to filter editor.": "切换到过滤器编辑器。",
"Delete filter block": "删除过滤器块",
"Add value": "添加值",
"Remove array item": "移除数组项目",
"Press <1>ESC</1> to lose focus": "按 <1>ESC</1> 键退出焦点",
"Must provide protocol: <1>https://</1>": "必须提供协议:<1>https://</1>",
"Must provide protocol: <1>http://</1> or <3>https://</3>": "必须提供协议:<1>http://</1> 或 <3>https://</3>",
"CORS policy won't allow fetching resources served over http from https, use a <1>https://</1> domain": "CORS 策略不允许从 https 访问通过 http 提供的资源,请使用 <1>https://</1> 域名。",
"Layer": "图层",
"JSON Editor": "JSON编辑器",
"Delete": "删除",
"Duplicate": "复制",
"Show": "显示",
"Hide": "隐藏",
"Move layer up": "向上移动图层",
"Move layer down": "向下移动图层",
"Layer: {{layerId}}": "图层: {{layerId}}",
"Layers": "图层",
"Collapse": "折叠",
"Expand": "展开",
"Add Layer": "添加图层",
"Zoom:": "缩放:",
"Search": "搜索",
"Close popup": "关闭弹出窗口",
"cursor:": "光标:",
"center:": "中心:",
"rotation:": "旋转:",
"Close modal": "关闭模态框",
"Debug": "调试",
"Options": "选项",
"<0>Open in OSM</0> — Opens the current view on openstreetmap.org": "在 openstreetmap.org 打开当前视图",
"Export Style": "导出样式",
"Download Style": "下载样式",
"Download a JSON style to your computer.": "将JSON样式下载到您的电脑。",
"Download HTML": "下载HTML",
"Cancel": "取消",
"Open Style": "打开样式",
"Upload Style": "上传样式",
"Upload a JSON style from your computer.": "从您的电脑上传JSON样式。",
"Style file": "样式文件",
"Upload": "上传",
"Load from URL": "从URL加载",
"Load from a URL. Note that the URL must have <1>CORS enabled</1>.": "从URL加载。注意URL必须启用 <1>CORS</1>。",
"Style URL": "样式URL",
"Enter URL...": "输入URL...",
"Gallery Styles": "画廊样式",
"Open one of the publicly available styles to start from.": "打开一个公开可用的样式开始。",
"Loading style": "正在加载样式",
"Loading: {{requestUrl}}": "正在加载: {{requestUrl}}",
"Name": "名称",
"Owner": "所有者",
"Owner ID of the style. Used by Mapbox or future style APIs.": "样式的所有者ID。由Mapbox或未来的样式API使用。",
"Sprite URL": "精灵URL",
"Glyphs URL": "字形URL",
"Center": "中心",
"Bearing": "方位",
"Pitch": "俯仰角",
"Light anchor": "光源锚点",
"Light color": "光源颜色",
"Light intensity": "光源强度",
"Light position": "光源位置",
"Terrain source": "地形源",
"Terrain exaggeration": "地形夸张",
"Transition delay": "过渡延迟",
"Transition duration": "过渡持续时间",
"Open Layers (experimental)": "开放图层(实验性)",
"Shortcuts menu": "快捷方式菜单",
"Open modal": "打开模态框",
"Export modal": "导出模态框",
"Data Sources modal": "数据源模态框",
"Style Settings modal": "样式设置模态框",
"Toggle inspect": "切换检查",
"Focus map": "聚焦地图",
"Debug modal": "调试模态框",
"Increase the zoom level by 1.": "将缩放级别增加1。",
"Increase the zoom level by 2.": "将缩放级别增加2。",
"Decrease the zoom level by 1.": "将缩放级别减少1。",
"Decrease the zoom level by 2.": "将缩放级别减少2。",
"Pan up by 100 pixels.": "向上平移100像素。",
"Pan down by 100 pixels.": "向下平移100像素。",
"Pan left by 100 pixels.": "向左平移100像素。",
"Pan right by 100 pixels.": "向右平移100像素。",
"Increase the rotation by 15 degrees.": "将旋转角度增加15度。",
"Decrease the rotation by 15 degrees.": "将旋转角度减少15度。",
"Increase the pitch by 10 degrees.": "将俯仰角增加10度。",
"Decrease the pitch by 10 degrees.": "将俯仰角减少10度。",
"Shortcuts": "快捷键",
"Press <1>ESC</1> to lose focus of any active elements, then press one of:": "按下 <1>ESC</1> 退出任何活动元素的焦点,然后按下以下之一:",
"If the Map is in focused you can use the following shortcuts": "如果地图处于焦点状态,您可以使用以下快捷键",
"Remove '{{sourceId}}' source": "移除 '{{sourceId}}' 源",
"Source ID": "源ID",
"Unique ID that identifies the source and is used in the layer to reference the source.": "用于标识源并在图层中引用源的唯一ID。",
"Source Type": "源类型",
"GeoJSON (JSON)": "GeoJSON (JSON格式)",
"GeoJSON (URL)": "GeoJSON (URL格式)",
"Vector (TileJSON URL)": "矢量数据 (TileJSON URL)",
"Vector (XYZ URLs)": "矢量数据 (XYZ URLs)",
"Raster (TileJSON URL)": "栅格数据 (TileJSON URL)",
"Raster (XYZ URL)": "栅格数据 (XYZ URL)",
"Raster DEM (TileJSON URL)": "栅格高程数据 (TileJSON URL)",
"Raster DEM (XYZ URLs)": "栅格高程数据 (XYZ URLs)",
"Image": "图像",
"Video": "视频",
"Add Source": "添加源",
"Sources": "源列表",
"Active Sources": "激活的源",
"Choose Public Source": "选择公共源",
"Add one of the publicly available sources to your style.": "将公开可用的源之一添加到您的样式中。",
"Add New Source": "添加新源",
"Add a new source to your style. You can only choose the source type and id at creation time!": "向您的样式添加新源。在创建时您只能选择源类型和ID",
"TileJSON URL": "TileJSON URL",
"Tile URL": "瓦片URL",
"Coord top left": "左上角坐标",
"Coord top right": "右上角坐标",
"Coord bottom right": "右下角坐标",
"Coord bottom left": "左下角坐标",
"Image URL": "图像URL",
"Video URL": "视频URL",
"GeoJSON URL": "GeoJSON URL",
"GeoJSON": "GeoJSON",
"Cluster": "聚合",
"Encoding": "编码",
"Error:": "错误:",
"MapTiler Access Token": "MapTiler 访问令牌",
"Public access token for MapTiler Cloud.": "MapTiler Cloud 的公共访问令牌。",
"Thunderforest Access Token": "Thunderforest 访问令牌",
"Public access token for Thunderforest services.": "Thunderforest 服务的公共访问令牌。",
"Style Renderer": "样式渲染器",
"Choose the default Maputnik renderer for this style.": "为这种样式选择默认的Maputnik渲染器。",
"Layer options": "图层选项",
"Paint properties": "绘制属性",
"Layout properties": "布局属性",
"General layout properties": "常规布局属性",
"Text layout properties": "文本布局属性",
"Icon layout properties": "图标布局属性",
"Text paint properties": "文本绘制属性",
"Icon paint properties": "图标绘制属性"
}

View File

@@ -3,11 +3,6 @@
.maputnik-map__container {
background: white;
display: flex;
position: fixed !important;
top: $toolbar-height + $toolbar-offset;
right: 0;
bottom: 0;
height: calc(100% - #{$toolbar-height + $toolbar-offset});
width: $layout-map-width;
&--error {

View File

@@ -14,26 +14,28 @@
font-family: $font-family;
color: $color-white;
&-list {
&-main {
position: fixed;
bottom: 0;
height: calc(100% - #{$toolbar-height + $toolbar-offset});
top: $toolbar-height + $toolbar-offset;
left: 0;
right: 0;
z-index: 3;
display: flex;
}
&-list {
width: 200px;
background-color: $color-black;
}
&-drawer {
position: fixed;
bottom: 0;
height: calc(100% - #{$toolbar-height + $toolbar-offset});
top: $toolbar-height + $toolbar-offset;
left: 200px;
z-index: 1;
width: 370px;
background-color: $color-black;
// scroll-container is position: absolute
position: relative;
}
&-bottom {

View File

@@ -308,26 +308,6 @@
}
}
.maputnik-modal-survey {
width: 400px;
}
.maputnik-modal-survey__logo {
display: block;
margin: 0 auto;
height: 128px;
width: 128px;
}
.maputnik-modal-survey__description {
line-height: 1.5;
}
.maputnik-modal-survey__footnote {
color: $color-green;
margin-top: 16px;
}
.modal-settings {
width: 400px;
}