This commit is contained in:
prusswan
2025-02-19 11:10:04 +08:00
committed by GitHub
12 changed files with 176 additions and 9 deletions

View File

@@ -0,0 +1,44 @@
import { MaputnikDriver } from "./maputnik-driver";
describe("local file", () => {
const { when, get } = new MaputnikDriver();
beforeEach(() => {
when.setStyle("");
});
describe("PMTiles", () => {
it("valid file loads without error", () => {
const fileName = "polygon-z0.pmtiles"; // a small polygon located at Null Island
const stub = cy.stub();
cy.on('window:alert', stub);
get
.bySelector("file", "type")
.selectFile(`cypress/fixtures/${fileName}`, { force: true });
when.wait(200);
cy.then(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(stub).to.not.have.been.called;
});
});
it("invalid file results in error", () => {
const fileName = "example-style.json";
const stub = cy.stub();
cy.on('window:alert', stub);
get
.bySelector("file", "type")
.selectFile(`cypress/fixtures/${fileName}`, { force: true });
when.wait(200);
cy.then(() => {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
expect(stub).to.be.called;
expect(stub.getCall(0).args[0]).to.contain('File type is not supported');
});
})
});
});

Binary file not shown.

43
package-lock.json generated
View File

@@ -51,6 +51,7 @@
"react-collapse": "^5.1.1",
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-dropzone": "^14.3.5",
"react-file-reader-input": "^2.0.0",
"react-i18next": "^15.4.0",
"react-icon-base": "^2.1.2",
@@ -3838,6 +3839,14 @@
"node": ">= 4.0.0"
}
},
"node_modules/attr-accept": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
"engines": {
"node": ">=4"
}
},
"node_modules/available-typed-arrays": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
@@ -6238,6 +6247,17 @@
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
},
"node_modules/file-selector": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
"dependencies": {
"tslib": "^2.7.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -10656,6 +10676,22 @@
"react": "^18.3.1"
}
},
"node_modules/react-dropzone": {
"version": "14.3.5",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.5.tgz",
"integrity": "sha512-9nDUaEEpqZLOz5v5SUcFA0CjM4vq8YbqO0WRls+EYT7+DvxUdzDPKNCPLqGfj3YL9MsniCLCD4RFA6M95V6KMQ==",
"dependencies": {
"attr-accept": "^2.2.4",
"file-selector": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">= 10.13"
},
"peerDependencies": {
"react": ">= 16.8 || 18.0.0"
}
},
"node_modules/react-file-reader-input": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-file-reader-input/-/react-file-reader-input-2.0.0.tgz",
@@ -12542,10 +12578,9 @@
}
},
"node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"dev": true
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
},
"node_modules/tunnel-agent": {
"version": "0.6.0",

View File

@@ -65,6 +65,7 @@
"react-collapse": "^5.1.1",
"react-color": "^2.19.3",
"react-dom": "^18.2.0",
"react-dropzone": "^14.3.5",
"react-file-reader-input": "^2.0.0",
"react-i18next": "^15.4.0",
"react-icon-base": "^2.1.2",

View File

@@ -8,7 +8,7 @@ import get from 'lodash.get'
import {unset} from 'lodash'
import {arrayMoveMutable} from 'array-move'
import hash from "string-hash";
import { PMTiles } from "pmtiles";
import { FileSource, PMTiles } from 'pmtiles';
import {Map, LayerSpecification, StyleSpecification, ValidationError, SourceSpecification} from 'maplibre-gl'
import {latest, validateStyleMin} from '@maplibre/maplibre-gl-style-spec'
@@ -131,6 +131,7 @@ type AppState = {
debug: boolean
}
fileHandle: FileSystemFileHandle | null
localPMTiles: PMTiles | null
}
export default class App extends React.Component<any, AppState> {
@@ -287,6 +288,7 @@ export default class App extends React.Component<any, AppState> {
debugToolbox: false,
},
fileHandle: null,
localPMTiles: null
}
this.layerWatcher = new LayerWatcher({
@@ -761,6 +763,7 @@ export default class App extends React.Component<any, AppState> {
onChange={this.onMapChange}
options={this.state.maplibreGlDebugOptions}
inspectModeEnabled={this.state.mapState === "inspect"}
localPMTiles={this.state.localPMTiles}
highlightedLayer={this.state.mapStyle.layers[this.state.selectedLayerIndex]}
onLayerSelect={this.onLayerSelect} />
}
@@ -902,6 +905,12 @@ export default class App extends React.Component<any, AppState> {
});
}
onLocalPMTilesSelected = (file: File) => {
this.setState({
localPMTiles: new PMTiles(new FileSource(file))
})
}
render() {
const layers = this.state.mapStyle.layers || []
const selectedLayer = layers.length > 0 ? layers[this.state.selectedLayerIndex] : undefined
@@ -916,6 +925,7 @@ export default class App extends React.Component<any, AppState> {
onStyleOpen={this.onStyleChanged}
onSetMapState={this.setMapState}
onToggleModal={this.toggleModal.bind(this)}
onLocalPMTilesSelected={this.onLocalPMTilesSelected}
/>
const layerList = <LayerList

View File

@@ -17,6 +17,8 @@ import maputnikLogo from 'maputnik-design/logos/logo-color.svg?inline'
import { withTranslation, WithTranslation } from 'react-i18next';
import { supportedLanguages } from '../i18n';
import { default as Dropzone, FileRejection } from 'react-dropzone';
// 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();
const colorAccessibilityFiltersEnabled = ['chrome', 'firefox'].indexOf(browser!.name) > -1;
@@ -103,6 +105,7 @@ type AppToolbarInternalProps = {
onSetMapState(mapState: MapState): unknown
mapState?: MapState
renderer?: string
onLocalPMTilesSelected(file: File): unknown
} & WithTranslation;
class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
@@ -134,6 +137,21 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
}
}
onFileSelected = (e: File[]) => {
const file = e[0];
this.props.onLocalPMTilesSelected(file);
}
onFileRejected = (r: FileRejection[]) => {
const errorMessageLine = r.map(e => {
return e.errors.map(f => f.message).join("\n")
}).join("\n");
console.error("Dropzone file rejected:", errorMessageLine);
const alertMessage = this.props.t("File type is not supported");
alert(alertMessage);
}
render() {
const t = this.props.t;
const views = [
@@ -174,6 +192,10 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
},
];
const acceptedFileTypes = {
'application/octet-stream': [".pmtiles"]
}
const currentView = views.find((view) => {
return view.id === this.props.mapState;
});
@@ -289,6 +311,15 @@ class AppToolbarInternal extends React.Component<AppToolbarInternalProps> {
<MdHelpOutline />
<IconText>{t("Help")}</IconText>
</ToolbarLink>
<Dropzone onDropAccepted={this.onFileSelected} onDropRejected={this.onFileRejected} accept={acceptedFileTypes}>
{({getRootProps, getInputProps}) => (
<div {...getRootProps({className: 'dropzone maputnik-toolbar-link'})}>
<input {...getInputProps()} />
{t("Drop PMTiles file here")}
</div>
)}
</Dropzone>
</div>
</div>
</nav>

View File

@@ -15,7 +15,7 @@ import MaplibreGeocoder, { MaplibreGeocoderApi, MaplibreGeocoderApiConfig } from
import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css';
import { withTranslation, WithTranslation } from 'react-i18next'
import i18next from 'i18next'
import { Protocol } from "pmtiles";
import { PMTiles, Protocol } from "pmtiles";
function renderPopup(popup: JSX.Element, mountNode: ReactDOM.Container): HTMLElement {
ReactDOM.render(popup, mountNode);
@@ -66,6 +66,7 @@ type MapMaplibreGlInternalProps = {
}
replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification
onChange(value: {center: LngLat, zoom: number}): unknown
localPMTiles: PMTiles | null;
} & WithTranslation;
type MapMaplibreGlState = {
@@ -74,8 +75,16 @@ type MapMaplibreGlState = {
geocoder: MaplibreGeocoder | null;
zoomControl: ZoomControl | null;
zoom?: number;
pmtilesProtocol: Protocol | null;
};
interface Metadata {
name?: string;
type?: string;
tilestats?: unknown;
vector_layers: LayerSpecification[];
}
class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps, MapMaplibreGlState> {
static defaultProps = {
onMapLoaded: () => {},
@@ -93,6 +102,7 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
inspect: null,
geocoder: null,
zoomControl: null,
pmtilesProtocol: new Protocol({metadata: true})
}
i18next.on('languageChanged', () => {
this.forceUpdate();
@@ -134,7 +144,23 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
this.state.inspect!.render();
}, 500);
}
if (this.props.localPMTiles) {
const file = this.props.localPMTiles;
this.state.pmtilesProtocol!.add(file); // this is necessary for non-HTTP sources
if (map) {
(file.getMetadata() as Promise<Metadata>).then(metadata => {
const layerNames = metadata.vector_layers.map((e: LayerSpecification) => e.id);
// used by maplibre-gl-inspect to pick up inspectable layers
map.style.sourceCaches["source"]._source.vectorLayerIds = layerNames;
}).catch( e => {
console.error(`${this.props.t('Error in reading local PMTiles file')}: ${e}`);
});
}
}
}
componentDidMount() {
@@ -149,8 +175,8 @@ class MapMaplibreGlInternal extends React.Component<MapMaplibreGlInternalProps,
localIdeographFontFamily: false
} satisfies MapOptions;
const protocol = new Protocol({metadata: true});
MapLibreGl.addProtocol("pmtiles",protocol.tile);
MapLibreGl.addProtocol("pmtiles", this.state.pmtilesProtocol!.tile);
const map = new MapLibreGl.Map(mapOpts);
const mapViewChange = () => {

View File

@@ -18,6 +18,7 @@
"Convert property to data function": "Eigenschaft in eine Datenfunktion umwandeln",
"Layer <1>{formatLayerId(layerId)}</1>: {parsed.data.message}": "Ebene <1>{formatLayerId(layerId)}</1>: {parsed.data.message}",
"switch to layer": "zur Ebene wechseln",
"File type is not supported": "__STRING_NOT_TRANSLATED__",
"Map": "Karte",
"Inspect": "Untersuchen",
"Deuteranopia filter": "Deuteranopie-Filter",
@@ -35,6 +36,7 @@
"View": "Ansicht",
"Color accessibility": "Farbzugänglichkeit",
"Help": "Hilfe",
"Drop PMTiles file here": "__STRING_NOT_TRANSLATED__",
"Comments for the current layer. This is non-standard and not in the spec.": "Kommentare zur aktuellen Ebene. Das ist nicht standardmäßig und nicht in der Spezifikation.",
"Comments": "Kommentare",
"Comment...": "Dein Kommentar...",
@@ -72,6 +74,7 @@
"Collapse": "Einklappen",
"Expand": "Ausklappen",
"Add Layer": "Ebene hinzufügen",
"Error in reading local PMTiles file": "__STRING_NOT_TRANSLATED__",
"Search": "Suche",
"Zoom:": "Zoom:",
"Close popup": "Popup schließen",
@@ -81,6 +84,7 @@
"Close modal": "Modale Fenster schließen",
"Debug": "Debug",
"Options": "Optionen",
"<0>Open in OSM</0> &mdash; Opens the current view on openstreetmap.org": "__STRING_NOT_TRANSLATED__",
"Save Style": "Stil Speichern",
"Save the JSON style to your computer.": "Speichere den JSON Stil auf deinem Computer.",
"Save as": "Speichern unter",

View File

@@ -18,6 +18,7 @@
"Convert property to data function": "Convertir la propriété en fonction de données",
"Layer <1>{formatLayerId(layerId)}</1>: {parsed.data.message}": "Calque <1>{formatLayerId(layerId)}</1> : {parsed.data.message}",
"switch to layer": "changer de calque",
"File type is not supported": "__STRING_NOT_TRANSLATED__",
"Map": "Carte",
"Inspect": "Inspecter",
"Deuteranopia filter": "Filtre Deutéranopie",
@@ -35,6 +36,7 @@
"View": "Vue",
"Color accessibility": "Accessibilité des couleurs",
"Help": "Aide",
"Drop PMTiles file here": "__STRING_NOT_TRANSLATED__",
"Comments for the current layer. This is non-standard and not in the spec.": "Commentaires pour le calque actuel. Ceci n'est pas standard et n'est pas dans la spécification.",
"Comments": "Commentaires",
"Comment...": "Votre commentaire...",
@@ -72,6 +74,7 @@
"Collapse": "Réduire",
"Expand": "Développer",
"Add Layer": "Ajouter un calque",
"Error in reading local PMTiles file": "__STRING_NOT_TRANSLATED__",
"Search": "Recherche",
"Zoom:": "Zoom :",
"Close popup": "Fermer la fenêtre",
@@ -81,6 +84,7 @@
"Close modal": "Fermer la fenêtre modale",
"Debug": "Déboguer",
"Options": "Options",
"<0>Open in OSM</0> &mdash; Opens the current view on openstreetmap.org": "__STRING_NOT_TRANSLATED__",
"Save Style": "Enregistrer le style",
"Save the JSON style to your computer.": "Enregistrer le style JSON sur votre ordinateur.",
"Save as": "Enregistrer sous",

View File

@@ -18,6 +18,7 @@
"Convert property to data function": "המרה לפונקציית מידע",
"Layer <1>{formatLayerId(layerId)}</1>: {parsed.data.message}": "שכבה <1>{formatLayerId(layerId)}</1>: {parsed.data.message}",
"switch to layer": "שינוי לשכבה",
"File type is not supported": "__STRING_NOT_TRANSLATED__",
"Map": "מפה",
"Inspect": "בדיקה",
"Deuteranopia filter": "Deuteranopia filter",
@@ -35,6 +36,7 @@
"View": "תצוגה",
"Color accessibility": "נגישות צבעים",
"Help": "עזרה",
"Drop PMTiles file here": "__STRING_NOT_TRANSLATED__",
"Comments for the current layer. This is non-standard and not in the spec.": "הערות על השכבה הנוכחית. זה לא חלק מהספסיפיקציות",
"Comments": "הערות",
"Comment...": "הערה...",
@@ -72,6 +74,7 @@
"Collapse": "הקטנה",
"Expand": "הגדלה",
"Add Layer": "הוספת שכבה",
"Error in reading local PMTiles file": "__STRING_NOT_TRANSLATED__",
"Search": "חיפוש",
"Zoom:": "זום:",
"Close popup": "סגירת החלון",
@@ -81,6 +84,7 @@
"Close modal": "סגירת חלונית",
"Debug": "דיבאג",
"Options": "אפשרויות",
"<0>Open in OSM</0> &mdash; Opens the current view on openstreetmap.org": "__STRING_NOT_TRANSLATED__",
"Save Style": "שמירת הסטייל",
"Save the JSON style to your computer.": "שמירת הסטייל JSON במחשב שלך.",
"Save as": "שמירה בשם",

View File

@@ -18,6 +18,7 @@
"Convert property to data function": "プロパティをデータ関数に変換する",
"Layer <1>{formatLayerId(layerId)}</1>: {parsed.data.message}": "レイヤ<1>{formatLayerId(layerId)}</1>: {parsed.data.message}",
"switch to layer": "レイヤへ切替",
"File type is not supported": "__STRING_NOT_TRANSLATED__",
"Map": "地図",
"Inspect": "検査",
"Deuteranopia filter": "緑色盲フィルタ",
@@ -35,6 +36,7 @@
"View": "表示",
"Color accessibility": "色のアクセシビリティ",
"Help": "ヘルプ",
"Drop PMTiles file here": "__STRING_NOT_TRANSLATED__",
"Comments for the current layer. This is non-standard and not in the spec.": "現在のレイヤーのコメント。注意:この機能は標準ではないため、他のライブラリとの互換性状況はわかりません。",
"Comments": "コメント",
"Comment...": "コメントを書く",
@@ -72,6 +74,7 @@
"Collapse": "畳む",
"Expand": "展開",
"Add Layer": "レイヤー追加",
"Error in reading local PMTiles file": "__STRING_NOT_TRANSLATED__",
"Search": "検索",
"Zoom:": "ズーム:",
"Close popup": "ポップアップを閉じる",
@@ -81,6 +84,7 @@
"Close modal": "モーダルを閉じる",
"Debug": "デバッグ",
"Options": "設定",
"<0>Open in OSM</0> &mdash; Opens the current view on openstreetmap.org": "__STRING_NOT_TRANSLATED__",
"Save Style": "スタイルを保存",
"Save the JSON style to your computer.": "JSONスタイルをコンピュータに保存します。",
"Save as": "名前を付けて保存",

View File

@@ -18,6 +18,7 @@
"Convert property to data function": "将属性转换为数据函数",
"Layer <1>{formatLayerId(layerId)}</1>: {parsed.data.message}": "图层<1>{formatLayerId(layerId)}</1>: {parsed.data.message}",
"switch to layer": "切换到图层",
"File type is not supported": "__STRING_NOT_TRANSLATED__",
"Map": "地图",
"Inspect": "检查",
"Deuteranopia filter": "绿色盲滤镜",
@@ -35,6 +36,7 @@
"View": "视图",
"Color accessibility": "颜色可访问性",
"Help": "帮助",
"Drop PMTiles file here": "__STRING_NOT_TRANSLATED__",
"Comments for the current layer. This is non-standard and not in the spec.": "当前图层的注释。注意:这不是标准功能,可能与其他库不兼容。",
"Comments": "注释",
"Comment...": "写注释...",
@@ -72,6 +74,7 @@
"Collapse": "折叠",
"Expand": "展开",
"Add Layer": "添加图层",
"Error in reading local PMTiles file": "__STRING_NOT_TRANSLATED__",
"Search": "搜索",
"Zoom:": "缩放:",
"Close popup": "关闭弹出窗口",
@@ -81,6 +84,7 @@
"Close modal": "关闭模态框",
"Debug": "调试",
"Options": "选项",
"<0>Open in OSM</0> &mdash; Opens the current view on openstreetmap.org": "__STRING_NOT_TRANSLATED__",
"Save Style": "保存样式",
"Save the JSON style to your computer.": "将JSON样式保存到您的计算机。",
"Save as": "另存为",