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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions cypress/e2e/local-file.cy.ts
Original file line number Diff line number Diff line change
@@ -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 added cypress/fixtures/polygon-z0.pmtiles
Binary file not shown.
43 changes: 39 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
12 changes: 11 additions & 1 deletion src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -131,6 +131,7 @@ type AppState = {
debug: boolean
}
fileHandle: FileSystemFileHandle | null
localPMTiles: PMTiles | null
}

export default class App extends React.Component<any, AppState> {
Expand Down Expand Up @@ -287,6 +288,7 @@ export default class App extends React.Component<any, AppState> {
debugToolbox: false,
},
fileHandle: null,
localPMTiles: null
}

this.layerWatcher = new LayerWatcher({
Expand Down Expand Up @@ -741,6 +743,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} />
}
Expand Down Expand Up @@ -881,6 +884,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
Expand All @@ -895,6 +904,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
Expand Down
31 changes: 31 additions & 0 deletions src/components/AppToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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 => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it critical to collect all the error messages?

Copy link
Author

Choose a reason for hiding this comment

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

I can just show the first error and apply translation for it, if that is okay with you.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Are you planning on translating all the errors?
I would simply present a generic error message to the user about failure to open the provided file.

Copy link
Author

Choose a reason for hiding this comment

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

I think I can log the errors from react-dropzone to console, and show a translatable error message to the user.

return e.errors.map(f => f.message).join("\n")
}).join("\n");
console.error("Dropzone file rejected:", errorMessageLine);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't see a reason to both print to the console and notify the user, so this can be removed I guess.


const alertMessage = this.props.t("File type is not supported");
alert(alertMessage);
}

render() {
const t = this.props.t;
const views = [
Expand Down Expand Up @@ -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;
});
Expand Down Expand Up @@ -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>
Expand Down
34 changes: 30 additions & 4 deletions src/components/MapMaplibreGl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -66,6 +66,7 @@ type MapMaplibreGlInternalProps = {
}
replaceAccessTokens(mapStyle: StyleSpecification): StyleSpecification
onChange(value: {center: LngLat, zoom: number}): unknown
localPMTiles: PMTiles | null;
} & WithTranslation;

type MapMaplibreGlState = {
Expand All @@ -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: () => {},
Expand All @@ -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();
Expand Down Expand Up @@ -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() {
Expand All @@ -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 = () => {
Expand Down
4 changes: 4 additions & 0 deletions src/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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...",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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...",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Loading