├── mapedit.png
├── maplist.png
├── img
├── icon.png
├── no_image.png
└── add_image.png
├── assets
├── win
│ └── icon_win.ico
└── mac
│ └── icon_mac.icns
├── frontend
├── fonts
│ ├── glyphicons-halflings-regular.eot
│ ├── glyphicons-halflings-regular.ttf
│ ├── glyphicons-halflings-regular.woff
│ └── glyphicons-halflings-regular.woff2
├── src
│ ├── applist.js
│ ├── model
│ │ ├── language.js
│ │ └── map.js
│ ├── settings.js
│ └── maplist.js
├── api
│ ├── mapupload.js
│ ├── dataupload.js
│ ├── wmts_generator.js
│ ├── dialog.js
│ ├── preload.js
│ ├── maplist.js
│ ├── settings.js
│ └── mapedit.js
├── lib
│ └── underscore_extension.js
└── vue
│ └── header.vue
├── backend
├── src
│ ├── uploadTest.js
│ ├── dialog.js
│ ├── dataupload.js
│ ├── mapupload.js
│ ├── main.js
│ ├── settings.js
│ ├── wmts_generator.js
│ ├── maplist.js
│ └── mapedit.js
└── lib
│ ├── os_arch.js
│ ├── progress_reporter.js
│ ├── ui_thumbnail.js
│ ├── utils.js
│ └── nedb_accessor.js
├── script
└── notarize
│ ├── entitlements.mac.plist
│ └── notarize.js
├── .gitignore
├── .github
└── FUNDING.yml
├── .eslintrc.json
├── html
├── applist.html
├── settings.html
├── maplist.html
└── mapedit.html
├── README.md
├── gulpfile.js
├── audit_result.txt
├── webpack.config.js
├── tileCutter.js
├── package.json
├── css
├── theme.css
└── non-responsive.css
├── locales
├── ja
│ └── translation.json
└── en
│ └── translation.json
└── LICENSE
/mapedit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4history/MaplatEditor/HEAD/mapedit.png
--------------------------------------------------------------------------------
/maplist.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4history/MaplatEditor/HEAD/maplist.png
--------------------------------------------------------------------------------
/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4history/MaplatEditor/HEAD/img/icon.png
--------------------------------------------------------------------------------
/img/no_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4history/MaplatEditor/HEAD/img/no_image.png
--------------------------------------------------------------------------------
/img/add_image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4history/MaplatEditor/HEAD/img/add_image.png
--------------------------------------------------------------------------------
/assets/win/icon_win.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4history/MaplatEditor/HEAD/assets/win/icon_win.ico
--------------------------------------------------------------------------------
/assets/mac/icon_mac.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4history/MaplatEditor/HEAD/assets/mac/icon_mac.icns
--------------------------------------------------------------------------------
/frontend/fonts/glyphicons-halflings-regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4history/MaplatEditor/HEAD/frontend/fonts/glyphicons-halflings-regular.eot
--------------------------------------------------------------------------------
/frontend/fonts/glyphicons-halflings-regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4history/MaplatEditor/HEAD/frontend/fonts/glyphicons-halflings-regular.ttf
--------------------------------------------------------------------------------
/frontend/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4history/MaplatEditor/HEAD/frontend/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/frontend/fonts/glyphicons-halflings-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code4history/MaplatEditor/HEAD/frontend/fonts/glyphicons-halflings-regular.woff2
--------------------------------------------------------------------------------
/backend/src/uploadTest.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const mapupload = require('./mapupload'); // eslint-disable-line no-undef
4 |
5 | mapupload.init();
6 |
7 | mapupload.imageCutter('test.png');
--------------------------------------------------------------------------------
/script/notarize/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-unsigned-executable-memory
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .env
3 | node_modules
4 | test*
5 | .serverless
6 | *.log
7 | warperpass.json
8 | s3bucket.json
9 | EXGW/s3
10 | local.properties
11 | mobile_android.iml
12 | .gradle
13 | app.iml
14 | build
15 | project.xcworkspace
16 | xcuserdata
17 | *.qgs
18 | *Backward*.json
19 | *Forward*.json
20 | *-darwin-x64
21 | *-win32-x64
22 | *-darwin.zip
23 | *-win32.zip
24 | out
25 | dist
26 |
27 | # Open/Keep
28 | .git.keep
29 | .git.open
--------------------------------------------------------------------------------
/frontend/src/applist.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import {Language} from './model/language';
3 | import Header from '../vue/header.vue';
4 | let langObj;
5 |
6 | async function initRun() {
7 | langObj = await Language.getSingleton();
8 | new Vue({
9 | i18n: langObj.vi18n,
10 | el: '#container',
11 | template: '#applist-vue-template',
12 | components: {
13 | "header-template": Header
14 | }
15 | });
16 | }
17 |
18 | initRun();
19 |
--------------------------------------------------------------------------------
/backend/lib/os_arch.js:
--------------------------------------------------------------------------------
1 | const pf = process.platform; // eslint-disable-line no-undef
2 | const arch = process.arch; // eslint-disable-line no-undef
3 |
4 | module.exports = function (arch_specified) { // eslint-disable-line no-undef
5 | const arch_decided = arch_specified || arch;
6 | return [
7 | pf === "win32" ? "win" : pf === "darwin" ? "mac" : "",
8 | arch_decided === "x64" ? "x64" : arch_decided === "arm64" ? "arm" : "",
9 | pf,
10 | arch_decided
11 | ];
12 | };
--------------------------------------------------------------------------------
/backend/src/dialog.js:
--------------------------------------------------------------------------------
1 | const {ipcMain, dialog} = require('electron'); // eslint-disable-line no-undef
2 |
3 | let initialized = false;
4 |
5 | module.exports = { // eslint-disable-line no-undef
6 | init() {
7 | if (!initialized) {
8 | initialized = true;
9 | ipcMain.on('dialog_request', async (ev, content) => {
10 | const resp = await dialog.showMessageBox(content);
11 | ev.reply("dialog_request_finished", resp);
12 | });
13 | }
14 | },
15 | }
--------------------------------------------------------------------------------
/frontend/api/mapupload.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer, contextBridge } = require('electron'); // eslint-disable-line no-undef
2 |
3 | let initialized = false;
4 |
5 | const apis = {
6 | async showMapSelectDialog(mapImageRepl) {
7 | ipcRenderer.send('mapupload_showMapSelectDialog', mapImageRepl);
8 | },
9 | on(channel, callback) {
10 | ipcRenderer.on(`mapupload_${channel}`, (event, argv) => {
11 | callback(event, argv);
12 | });
13 | }
14 | };
15 |
16 | module.exports = { // eslint-disable-line no-undef
17 | init() {
18 | if (!initialized) {
19 | contextBridge.exposeInMainWorld('mapupload', apis);
20 | initialized = true;
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/frontend/api/dataupload.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer, contextBridge } = require('electron'); // eslint-disable-line no-undef
2 |
3 | let initialized = false;
4 |
5 | const apis = {
6 | async showDataSelectDialog(mapImageRepl) {
7 | ipcRenderer.send('dataupload_showDataSelectDialog', mapImageRepl);
8 | },
9 | on(channel, callback) {
10 | ipcRenderer.on(`dataupload_${channel}`, (event, argv) => {
11 | callback(event, argv);
12 | });
13 | }
14 | };
15 |
16 | module.exports = { // eslint-disable-line no-undef
17 | init() {
18 | if (!initialized) {
19 | contextBridge.exposeInMainWorld('dataupload', apis);
20 | initialized = true;
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/frontend/api/wmts_generator.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer, contextBridge } = require('electron'); // eslint-disable-line no-undef
2 |
3 | let initialized = false;
4 |
5 | const apis = {
6 | async generate(mapID, width, height, tinSerial, extKey, hash) {
7 | ipcRenderer.send('wmtsGen_generate', mapID, width, height, tinSerial, extKey, hash);
8 | },
9 | on(channel, callback) {
10 | ipcRenderer.on(`wmtsGen_${channel}`, (event, argv) => {
11 | callback(event, argv);
12 | });
13 | }
14 | };
15 |
16 | module.exports = { // eslint-disable-line no-undef
17 | init() {
18 | if (!initialized) {
19 | contextBridge.exposeInMainWorld('wmtsGen', apis);
20 | initialized = true;
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: maplat
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/frontend/api/dialog.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer, contextBridge } = require('electron'); // eslint-disable-line no-undef
2 |
3 | let initialized = false;
4 |
5 | const apis = {
6 | async showMessageBox(content) {
7 | console.log("!!!")
8 | console.log(content)
9 | return new Promise((res) => {
10 | console.log("AAA")
11 | console.log(content)
12 | ipcRenderer.once('dialog_request_finished', (ev, resp) => {
13 | res(resp);
14 | });
15 | ipcRenderer.send('dialog_request', content);
16 | });
17 | }
18 | };
19 |
20 | module.exports = { // eslint-disable-line no-undef
21 | init() {
22 | if (!initialized) {
23 | contextBridge.exposeInMainWorld('dialog', apis);
24 | initialized = true;
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/frontend/api/preload.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer, contextBridge } = require('electron'); // eslint-disable-line no-undef
2 |
3 | contextBridge.exposeInMainWorld('baseApi', {
4 | // Define on-demand require function for setting set of frontend and backend logics
5 | async require(module_name) {
6 | return new Promise((res) => {
7 | // Event listener that backend logic registration was finished
8 | ipcRenderer.once('require_ready', () => {
9 | res();
10 | });
11 | // Frontend logic registration
12 | const frontend = require(`./${module_name}`); // eslint-disable-line no-undef
13 | frontend.init();
14 | // Request for backend logic registration
15 | ipcRenderer.send('require', module_name);
16 | });
17 | }
18 | });
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint:recommended",
3 | "env": {
4 | "es6": true
5 | },
6 | "parserOptions": {
7 | "sourceType": "module",
8 | "ecmaVersion": 8
9 | },
10 | "rules": {
11 | "arrow-body-style": "error",
12 | "arrow-parens": "error",
13 | "arrow-spacing": "error",
14 | "generator-star-spacing": "error",
15 | "no-duplicate-imports": "error",
16 | "no-useless-computed-key": "error",
17 | "no-useless-constructor": "error",
18 | "no-useless-rename": "error",
19 | "no-var": "error",
20 | "object-shorthand": "error",
21 | "prefer-arrow-callback": "error",
22 | "prefer-const": "error",
23 | "prefer-rest-params": "error",
24 | "prefer-spread": "error",
25 | "prefer-template": "error",
26 | "rest-spread-spacing": "error",
27 | "template-curly-spacing": "error",
28 | "yield-star-spacing": "error"
29 | }
30 | }
--------------------------------------------------------------------------------
/frontend/api/maplist.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer, contextBridge } = require('electron'); // eslint-disable-line no-undef
2 |
3 | let initialized = false;
4 |
5 | const apis = {
6 | async start() {
7 | ipcRenderer.send('maplist_start');
8 | },
9 | async migration() {
10 | ipcRenderer.send('maplist_migration');
11 | },
12 | async request(condition, page) {
13 | ipcRenderer.send('maplist_request', condition, page);
14 | },
15 | async deleteOld() {
16 | ipcRenderer.send('maplist_deleteOld');
17 | },
18 | async delete(mapID, condition, page) {
19 | ipcRenderer.send('maplist_delete', mapID, condition, page);
20 | },
21 | on(channel, callback) {
22 | ipcRenderer.on(`maplist_${channel}`, (event, argv) => {
23 | callback(event, argv);
24 | });
25 | }
26 | };
27 |
28 | module.exports = { // eslint-disable-line no-undef
29 | init() {
30 | if (!initialized) {
31 | contextBridge.exposeInMainWorld('maplist', apis);
32 | initialized = true;
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/script/notarize/notarize.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const { notarize } = require('@electron/notarize');
3 | const mac_build = require('../../build_mac');
4 |
5 | exports.default = async function notarizing(context) {
6 | const { electronPlatformName, appOutDir } = context;
7 | const appName = context.packager.appInfo.productFilename;
8 |
9 | const isMac = electronPlatformName === 'darwin';
10 | if (!isMac) {
11 | console.log('Notarization is skipped on OSs other than macOS.');
12 | return;
13 | }
14 |
15 | const isPackageTest = !!process.env.PLM_PACKAGE_TEST;
16 | if (isPackageTest) {
17 | console.log('Notarization is skipped in package test.');
18 | return;
19 | }
20 |
21 | console.log('Started notarization.');
22 | await notarize({
23 | appBundleId: mac_build.appId,
24 | appPath: `${appOutDir}/${appName}.app`,
25 | appleId: process.env.APPLE_ID,
26 | appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
27 | });
28 | console.log('Finished notarization.');
29 | };
--------------------------------------------------------------------------------
/backend/lib/progress_reporter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class ProgressReporter {
4 | constructor(prefix, fullNumber, progressText, finishText) {
5 | this.prefix = prefix;
6 | this.fullnumber = fullNumber;
7 | this.progressText = progressText;
8 | this.finishText = finishText;
9 | this.percent = null;
10 | this.time = null;
11 | }
12 |
13 | update(ev, currentNumber) {
14 | const currentPercent = Math.floor(currentNumber * 100 / this.fullnumber);
15 | const currentTime = new Date();
16 | if (this.percent == null || this.time == null || currentPercent === 100 || currentPercent - this.percent > 5 || currentTime - this.time > 30000) {
17 | this.percent = currentPercent;
18 | this.time = currentTime;
19 | ev.reply(`${this.prefix}_taskProgress`, {
20 | percent: currentPercent,
21 | progress: `(${currentNumber}/${this.fullnumber})`,
22 | text: currentPercent === 100 && this.finishText ? this.finishText : this.progressText
23 | });
24 | }
25 | }
26 | }
27 |
28 | module.exports = ProgressReporter; // eslint-disable-line no-undef
--------------------------------------------------------------------------------
/html/applist.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Maplat Editor
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/backend/lib/ui_thumbnail.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const fs = require('fs-extra'); // eslint-disable-line no-undef
4 | const {Jimp} = require('../lib/utils'); // eslint-disable-line no-undef
5 |
6 | exports.make_thumbnail = async function(from, to, oldSpec) { // eslint-disable-line no-undef
7 | const extractor = async function(from, to) {
8 | const imageJimp = await Jimp.read(from);
9 |
10 | const width = imageJimp.bitmap.width;
11 | const height = imageJimp.bitmap.height;
12 | const w = width > height ? 52 : Math.ceil(52 * width / height);
13 | const h = width > height ? Math.ceil(52 * height / width) : 52;
14 |
15 | await imageJimp.resize(w, h).write(to);
16 | };
17 |
18 | if (oldSpec) {
19 | try {
20 | await fs.stat(oldSpec);
21 | await fs.move(oldSpec, to, {overwrite: true});
22 | } catch (noOldSpec) {
23 | if (noOldSpec.code === 'ENOENT'){
24 | try {
25 | await fs.stat(to);
26 | } catch (noTo) {
27 | if (noTo.code === 'ENOENT') {
28 | await extractor(from, to);
29 | } else throw noTo;
30 | }
31 | } else throw noOldSpec;
32 | }
33 | } else {
34 | await extractor(from, to);
35 | }
36 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MaplatEditor
2 | 
3 |
4 | [Maplat](https://github.com/code4history/Maplat/wiki) is the cool Historical Map/Illustrated Map Viewer.
5 | MaplatEditor is support project of Maplat, providing the data editor of Maplat.
6 | ***NOTE:*** This project is quite new, so now support Japanese GUI only.
7 |
8 | [Maplat](https://github.com/code4history/Maplat/wiki) は古地図/絵地図を歪める事なくGPSや正確な地図と連携させられるオープンソースプラットフォームです。
9 | MaplatEditorはMaplatのサポートプロジェクトで、データエディタを提供します。
10 |
11 | # パッケージ版 ([最新バージョン0.6.5](https://github.com/code4history/MaplatEditor/releases/tag/v0.6.5))
12 | * Windows用 (64ビット): https://github.com/code4history/MaplatEditor/releases/download/v0.6.5/MaplatEditor.Setup.0.6.5.exe
13 | * Mac用 (X64): https://github.com/code4history/MaplatEditor/releases/download/v0.6.5/MaplatEditor-0.6.5.dmg
14 | * Mac用 (Apple Silicon): Apple Silicon binary of 0.6.5 is not provided, because of electron-builder's bug. Please use intel binary instead.
15 |
16 | # GUI
17 | ## MapList (地図一覧)
18 |
19 | 
20 |
21 | ## MapEdit (地図編集)
22 |
23 | 
24 |
--------------------------------------------------------------------------------
/frontend/api/settings.js:
--------------------------------------------------------------------------------
1 | const { ipcRenderer, contextBridge } = require('electron'); // eslint-disable-line no-undef
2 |
3 | let initialized = false;
4 |
5 | const apis = {
6 | async lang() {
7 | return new Promise((res) => {
8 | // Event listener that setter of electron-json-storage was finished
9 | ipcRenderer.once('settings_lang_got', (ev, langVal) => {
10 | res(langVal);
11 | });
12 | // Request for backend logic for setter of electron-json-storage
13 | ipcRenderer.send('settings_lang');
14 | });
15 | },
16 | async setSetting(key, value) {
17 | return new Promise((res) => {
18 | ipcRenderer.once('settings_setSetting_finished', () => {
19 | res();
20 | });
21 | ipcRenderer.send('settings_setSetting', key, value);
22 | });
23 | },
24 | async getSetting(key) {
25 | return new Promise((res) => {
26 | ipcRenderer.once('settings_getSetting_finished', (ev, value) => {
27 | res(value);
28 | });
29 | ipcRenderer.send('settings_getSetting', key);
30 | });
31 | },
32 | async showSaveFolderDialog(current) {
33 | ipcRenderer.send('settings_showSaveFolderDialog', current);
34 | },
35 | on(channel, callback) {
36 | ipcRenderer.on(`settings_${channel}`, (event, argv) => {
37 | callback(event, argv);
38 | });
39 | }
40 | };
41 |
42 | module.exports = { // eslint-disable-line no-undef
43 | init() {
44 | if (!initialized) {
45 | contextBridge.exposeInMainWorld('settings', apis);
46 | initialized = true;
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | const gulp = require("gulp"); // eslint-disable-line no-undef
2 | const fs = require("fs-extra"); // eslint-disable-line no-undef
3 | const { execSync } = require('child_process'); // eslint-disable-line no-undef
4 |
5 | const minimist = require('minimist'); // eslint-disable-line no-undef
6 | const osArchFinder = require("./backend/lib/os_arch"); // eslint-disable-line no-undef
7 |
8 | gulp.task("git_switch", async () => {
9 | try {
10 | fs.statSync('.git.open');
11 | fs.moveSync('.git', '.git.keep');
12 | fs.moveSync('.git.open', '.git');
13 | } catch(e) {
14 | fs.moveSync('.git', '.git.open');
15 | fs.moveSync('.git.keep', '.git');
16 | }
17 | });
18 |
19 | gulp.task("exec", async () => {
20 | const commands = [];
21 | if (osArchFinder()[0] === "win") {
22 | commands.push("chcp 65001");
23 | }
24 | commands.push("npm run js_build");
25 | commands.push("npm run css_build");
26 | commands.push("electron .");
27 | execSync(commands.join(" && "), {stdio: 'inherit'});
28 | });
29 |
30 | gulp.task("build", async () => {
31 | const [os, arch_abbr, pf, arch] = getArchOption();
32 | const commands = [
33 | "npm run lint",
34 | "npm run js_build",
35 | "npm run css_build"
36 | ];
37 | const packege_cmd = `electron-builder --${os} --${arch} --config ./build_${os}.js`;
38 | console.log(packege_cmd);
39 | commands.push(packege_cmd);
40 | execSync(commands.join(" && "), {stdio: 'inherit'});
41 | });
42 |
43 | function getArchOption() {
44 | const options = minimist(process.argv.slice(2), { // eslint-disable-line no-undef
45 | string: 'arch'
46 | });
47 | return osArchFinder(options.arch);
48 | }
--------------------------------------------------------------------------------
/frontend/lib/underscore_extension.js:
--------------------------------------------------------------------------------
1 | import _ from 'underscore';
2 |
3 | _.deepClone = function(object) {
4 | var clone = _.clone(object);
5 |
6 | _.each(clone, function(value, key) {
7 | if (_.isObject(value)) {
8 | clone[key] = _.deepClone(value);
9 | }
10 | });
11 |
12 | return clone;
13 | };
14 |
15 | _.isDeepEqual = function(x, y) {
16 | if ( x === y ) return true;
17 | // if both x and y are null or undefined and exactly the same
18 |
19 | if ( ! ( x instanceof Object ) || ! ( y instanceof Object ) ) return false;
20 | // if they are not strictly equal, they both need to be Objects
21 |
22 | if ( x.constructor !== y.constructor ) return false;
23 | // they must have the exact same prototype chain, the closest we can do is
24 | // test there constructor.
25 |
26 | for ( var p in x ) {
27 | if ( ! x.hasOwnProperty( p ) ) continue;
28 | // other properties were tested using x.constructor === y.constructor
29 |
30 | if ( ! y.hasOwnProperty( p ) ) return false;
31 | // allows to compare x[ p ] and y[ p ] when set to undefined
32 |
33 | if ( x[p] === y[p] ) continue;
34 | // if they have the same strict value or identity then they are equal
35 |
36 | if ( typeof( x[p] ) !== 'object' ) return false;
37 | // Numbers, Strings, Functions, Booleans must be strictly equal
38 |
39 | if ( ! _.isDeepEqual( x[p], y[p] ) ) return false;
40 | // Objects and Arrays must be tested recursively
41 | }
42 |
43 | for ( p in y ) {
44 | if ( y.hasOwnProperty( p ) && ! x.hasOwnProperty( p ) ) return false;
45 | // allows x[ p ] to be set to undefined
46 | }
47 | return true;
48 | };
49 |
50 | export default _;
51 |
52 |
--------------------------------------------------------------------------------
/audit_result.txt:
--------------------------------------------------------------------------------
1 | # npm audit report
2 |
3 | glob-parent <5.1.2
4 | Severity: high
5 | Regular expression denial of service - https://github.com/advisories/GHSA-ww39-953v-wcq6
6 | fix available via `npm audit fix --force`
7 | Will install gulp@3.9.1, which is a breaking change
8 | node_modules/glob-parent
9 | chokidar 1.0.0-rc1 - 2.1.8
10 | Depends on vulnerable versions of glob-parent
11 | node_modules/chokidar
12 | glob-watcher >=3.0.0
13 | Depends on vulnerable versions of chokidar
14 | node_modules/glob-watcher
15 | gulp >=4.0.0
16 | Depends on vulnerable versions of glob-watcher
17 | node_modules/gulp
18 | glob-stream 5.3.0 - 6.1.0
19 | Depends on vulnerable versions of glob-parent
20 | node_modules/glob-stream
21 | vinyl-fs >=2.4.2
22 | Depends on vulnerable versions of glob-stream
23 | node_modules/vinyl-fs
24 |
25 | postcss <8.2.13
26 | Severity: moderate
27 | Regular Expression Denial of Service in postcss - https://github.com/advisories/GHSA-566m-qj78-rww5
28 | fix available via `npm audit fix --force`
29 | Will install vue-loader@17.0.0, which is a breaking change
30 | node_modules/@vue/component-compiler-utils/node_modules/postcss
31 | @vue/component-compiler-utils *
32 | Depends on vulnerable versions of postcss
33 | node_modules/@vue/component-compiler-utils
34 | vue-loader 15.0.0-beta.1 - 15.9.8
35 | Depends on vulnerable versions of @vue/component-compiler-utils
36 | node_modules/vue-loader
37 |
38 | 9 vulnerabilities (3 moderate, 6 high)
39 |
40 | To address issues that do not require attention, run:
41 | npm audit fix
42 |
43 | To address all issues (including breaking changes), run:
44 | npm audit fix --force
45 |
--------------------------------------------------------------------------------
/frontend/vue/header.vue:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
24 |
48 |
49 |
--------------------------------------------------------------------------------
/backend/lib/utils.js:
--------------------------------------------------------------------------------
1 | const path = require("path"); // eslint-disable-line no-undef
2 | const fileUrl = require("file-url"); // eslint-disable-line no-undef
3 | const storeHandler = require("@maplat/core/es5/source/store_handler"); // eslint-disable-line no-undef
4 | const fs = require('fs').promises // eslint-disable-line no-undef
5 | const Jimp = require('jimp'); // eslint-disable-line no-undef
6 | const JPEG = require('jpeg-js'); // eslint-disable-line no-undef
7 |
8 | Jimp.decoders['image/jpeg'] = (data) => JPEG.decode(data, {
9 | maxMemoryUsageInMB: 6144,
10 | maxResolutionInMP: 600
11 | });
12 |
13 | async function exists(filepath) {
14 | try {
15 | await fs.lstat(filepath);
16 | return true;
17 | } catch (e) {
18 | return false
19 | }
20 | }
21 |
22 | async function normalizeRequestData(json, thumbFolder) {
23 | let url_;
24 | const whReady = (json.width && json.height) || (json.compiled && json.compiled.wh);
25 | if (!whReady) return [json, ];
26 |
27 | await new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars
28 | if (json.url) {
29 | url_ = json.url;
30 | resolve();
31 | } else {
32 | fs.readdir(thumbFolder, (err, thumbs) => {
33 | if (!thumbs) {
34 | resolve();
35 | return;
36 | }
37 | for (let i=0; i {
17 | ipcRenderer.once('mapedit_getTmsListOfMapID_finished', (ev, list) => {
18 | res(list);
19 | });
20 | ipcRenderer.send('mapedit_getTmsListOfMapID', mapID);
21 | });
22 | },
23 | async getWmtsFolder() {
24 | return new Promise((res) => {
25 | ipcRenderer.once('mapedit_getWmtsFolder_finished', (ev, folder) => {
26 | res(folder);
27 | });
28 | ipcRenderer.send('mapedit_getWmtsFolder');
29 | });
30 | },
31 | async checkID(mapID) {
32 | ipcRenderer.send('mapedit_checkID', mapID);
33 | },
34 | async download(mapObject, tins) {
35 | ipcRenderer.send('mapedit_download', mapObject, tins);
36 | },
37 | async uploadCsv(csvRepl, csvUpSettings) {
38 | ipcRenderer.send('mapedit_uploadCsv', csvRepl, csvUpSettings);
39 | },
40 | async save(mapObject, tins) {
41 | ipcRenderer.send('mapedit_save', mapObject, tins);
42 | },
43 | on(channel, callback) {
44 | ipcRenderer.on(`mapedit_${channel}`, (event, argv) => {
45 | callback(event, argv);
46 | });
47 | },
48 | once(channel, callback) {
49 | ipcRenderer.once(`mapedit_${channel}`, (event, argv) => {
50 | callback(event, argv);
51 | });
52 | }
53 | };
54 |
55 | module.exports = { // eslint-disable-line no-undef
56 | init() {
57 | if (!initialized) {
58 | contextBridge.exposeInMainWorld('mapedit', apis);
59 | initialized = true;
60 | }
61 | }
62 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const { VueLoaderPlugin } = require('vue-loader');
2 |
3 | module.exports = {
4 | mode: 'production',
5 | devtool: 'source-map',
6 | entry: {
7 | maplist: './frontend/src/maplist.js',
8 | mapedit: './frontend/src/mapedit.js',
9 | applist: './frontend/src/applist.js',
10 | settings: './frontend/src/settings.js'
11 | },
12 | output: {
13 | path: `${__dirname}/frontend/dist`,
14 | filename: '[name].bundle.js'
15 | },
16 | resolve: {
17 | alias: {
18 | 'vue$': 'vue/dist/vue.esm.js'
19 | },
20 | symlinks: false,
21 | fallback: {
22 | crypto: require.resolve("crypto-browserify"),
23 | buffer: require.resolve("buffer"),
24 | stream: require.resolve("stream-browserify")
25 | }
26 | },
27 | plugins: [
28 | // make sure to include the plugin!
29 | new VueLoaderPlugin()
30 | ],
31 | module: {
32 | rules: [
33 | {
34 | test: /\.vue$/,
35 | loader: 'vue-loader',
36 | options: {
37 | loaders: {
38 | js: 'babel-loader'
39 | }
40 | }
41 | },
42 | {
43 | test: /\.js$/,
44 | exclude: /node_modules(?![\\/]@maplat)/,
45 | use: {
46 | loader: 'babel-loader',
47 | options: {
48 | "presets": [
49 | [
50 | "@babel/preset-env",
51 | {
52 | "useBuiltIns": "usage",
53 | "corejs": 3
54 | }
55 | ]
56 | ]
57 | }
58 | }
59 | },
60 | {
61 | test: /\.(jpg|jpeg|png)$/,
62 | exclude: /node_modules(?![\\/]@maplat)/,
63 | loader: 'file-loader',
64 | options: {
65 | outputPath: "images"
66 | }
67 | }
68 | ]
69 | },
70 | externals: [
71 | (function () {
72 | const IGNORES = [
73 | 'electron'
74 | ];
75 | return ({context, request}, callback) => {
76 | if (IGNORES.indexOf(request) >= 0) {
77 | return callback(null, "require('" + request + "')");
78 | }
79 | return callback();
80 | };
81 | })()
82 | ]
83 | };
84 |
--------------------------------------------------------------------------------
/frontend/src/model/language.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import i18next from 'i18next';
3 | import HttpApi from 'i18next-http-backend';
4 | import VueI18Next from "@panter/vue-i18next";
5 |
6 | let singleton;
7 |
8 | export class Language {
9 | constructor() {
10 | this.asyncReady = (async () => {
11 | await window.baseApi.require('settings'); // eslint-disable-line no-undef
12 | this.i18n = i18next.use(HttpApi); //backend.i18n;
13 | Vue.use(VueI18Next);
14 |
15 | const lang = await window.settings.lang(); // eslint-disable-line no-undef
16 | this.vi18n = new VueI18Next(this.i18n);
17 | this.t = await this.i18n.init({
18 | lng: lang,
19 | fallbackLng: 'en',
20 | backend: {
21 | loadPath: `../locales/{{lng}}/{{ns}}.json` // eslint-disable-line no-undef
22 | }
23 | });
24 | this.translate = (dataFragment) => {
25 | if (!dataFragment || typeof dataFragment != 'object') return dataFragment;
26 | const langs = Object.keys(dataFragment);
27 | let key = langs.reduce((prev, curr) => {
28 | if (curr === 'en' || !prev) {
29 | prev = dataFragment[curr];
30 | }
31 | return prev;
32 | }, null);
33 | key = (typeof key == 'string') ? key : `${key}`;
34 | if (this.i18n.exists(key, {ns: 'translation', nsSeparator: '__X__yX__X__'}))
35 | return this.t(key, {ns: 'translation', nsSeparator: '__X__yX__X__'});
36 | for (let i = 0; i < langs.length; i++) {
37 | const lang = langs[i];
38 | this.i18n.addResource(lang, 'translation', key, dataFragment[lang]);
39 | }
40 | return this.t(key, {ns: 'translation', nsSeparator: '__X__yX__X__'});
41 | };
42 |
43 | const items = document.querySelectorAll('.vi18n'); // eslint-disable-line no-undef
44 |
45 | items.forEach((el) => {
46 | new Vue({
47 | el, // HTMLElementをそのままelプロパティに渡す
48 | i18n: this.vi18n
49 | });
50 | });
51 | })();
52 | }
53 |
54 | static async getSingleton() {
55 | if (!singleton) {
56 | singleton = new Language();
57 | }
58 | await singleton.asyncReady;
59 | return singleton;
60 | }
61 | }
--------------------------------------------------------------------------------
/tileCutter.js:
--------------------------------------------------------------------------------
1 | const pf = process.platform;
2 | const canvasPath = pf == 'darwin' ? './assets/mac/canvas' : './assets/win/canvas';
3 | const { createCanvas, loadImage } = require(canvasPath);
4 | const fs = require('fs-extra');
5 |
6 | //=== Temp test data ===
7 | const imagePath = 'C:\\Users\\10467\\OneDrive\\MaplatEditor\\originals\\naramachi_yasui_bunko.jpg';
8 | const tileRoot = 'C:\\Users\\10467\\OneDrive\\MaplatEditor\\tiles';
9 | const mapID = 'naramachi_2';
10 | const extension = 'jpg';
11 |
12 | handleMaxZoom(imagePath, tileRoot, mapID, extension);
13 |
14 | async function handleMaxZoom(imagePath, tileRoot, mapID, extension) {
15 | const image = await loadImage(imagePath);
16 | const width = image.width;
17 | const height = image.height;
18 |
19 | const maxZoom = Math.ceil(Math.log(Math.max(width, height) / 256)/ Math.log(2));
20 |
21 | for (let z = maxZoom; z >= 0; z--) {
22 | const pw = Math.round(width / Math.pow(2, maxZoom - z));
23 | const ph = Math.round(height / Math.pow(2, maxZoom - z));
24 | for (let tx = 0; tx * 256 < pw; tx++) {
25 | const tw = (tx + 1) * 256 > pw ? pw - tx * 256 : 256;
26 | const sx = tx * 256 * Math.pow(2, maxZoom - z);
27 | const sw = (tx + 1) * 256 * Math.pow(2, maxZoom - z) > width ? width - sx : 256 * Math.pow(2, maxZoom - z);
28 | const tileFolder = `${tileRoot}\\${mapID}\\${z}\\${tx}`;
29 | await fs.ensureDir(tileFolder);
30 | for (let ty = 0; ty * 256 < ph; ty++) {
31 | const th = (ty + 1) * 256 > ph ? ph - ty * 256 : 256;
32 | const sy = ty * 256 * Math.pow(2, maxZoom - z);
33 | const sh = (ty + 1) * 256 * Math.pow(2, maxZoom - z) > height ? height - sy : 256 * Math.pow(2, maxZoom - z);
34 | const canvas = createCanvas(tw, th);
35 | const ctx = canvas.getContext('2d');
36 | ctx.drawImage(image, sx, sy, sw, sh, 0, 0, tw, th);
37 |
38 | const tileFile = `${tileFolder}\\${ty}.${extension}`;
39 |
40 | const jpgTile = canvas.toBuffer('image/jpeg', {quality: 0.9});
41 | await fs.outputFile(tileFile, jpgTile);
42 | }
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "MaplatEditor",
3 | "version": "0.6.5",
4 | "description": "",
5 | "main": "backend/src/main.js",
6 | "scripts": {
7 | "lint": "eslint backend/src/ && eslint frontend/src/",
8 | "js_build": "webpack",
9 | "css_build": "lessc --clean-css css/theme.css frontend/dist/maplateditor.css",
10 | "exec": "gulp exec",
11 | "build": "gulp build",
12 | "build_x": "gulp build --arch x64",
13 | "build_arm": "gulp build --arch arm64",
14 | "git_switch": "gulp git_switch"
15 | },
16 | "keywords": [],
17 | "author": "Code for History",
18 | "license": "Apache-2.0",
19 | "devDependencies": {
20 | "@babel/cli": "^7.17.10",
21 | "@babel/core": "^7.18.2",
22 | "@babel/polyfill": "^7.12.1",
23 | "@babel/preset-env": "^7.18.2",
24 | "@babel/register": "^7.17.7",
25 | "@electron/notarize": "^1.2.3",
26 | "babel-loader": "^8.2.5",
27 | "clean-css": "^5.3.0",
28 | "core-js": "^3.22.7",
29 | "dotenv": "^16.0.1",
30 | "electron": "^22.0.0",
31 | "electron-builder": "^23.6.0",
32 | "eslint": "^7.32.0",
33 | "gulp": "^4.0.2",
34 | "gulp-cli": "^2.3.0",
35 | "less": "^4.1.2",
36 | "vue-loader": "^15.10.1",
37 | "vue-template-compiler": "^2.7.14",
38 | "webpack": "^5.72.1",
39 | "webpack-cli": "^4.9.2"
40 | },
41 | "dependencies": {
42 | "@maplat/core": "^0.10.5",
43 | "@maplat/tin": "^0.9.4",
44 | "@panter/vue-i18next": "^0.15.2",
45 | "@seald-io/nedb": "^3.0.0",
46 | "@turf/turf": "^6.5.0",
47 | "about-window": "^1.15.2",
48 | "adm-zip": "^0.5.9",
49 | "bootstrap.native": "^2.0.27",
50 | "bootstrap3": "^3.3.5",
51 | "buffer": "^6.0.3",
52 | "caniuse-lite": "^1.0.30001431",
53 | "child_process": "^1.0.2",
54 | "crypto-browserify": "^3.12.0",
55 | "css-loader": "^5.2.6",
56 | "csv-parser": "^3.0.0",
57 | "csvtojson": "^2.0.10",
58 | "electron-json-storage": "^4.5.0",
59 | "file-loader": "^6.2.0",
60 | "file-url": "^3.0.0",
61 | "fs-extra": "^10.1.0",
62 | "i18next": "^21.8.4",
63 | "i18next-fs-backend": "^1.1.4",
64 | "i18next-http-backend": "^1.4.1",
65 | "i18next-xhr-backend": "^3.2.2",
66 | "jimp": "^0.16.1",
67 | "ol": "^6.14.1",
68 | "ol-contextmenu": "^4.1.0",
69 | "ol-geocoder": "^4.1.2",
70 | "ol-layerswitcher": "^3.8.3",
71 | "path": "^0.12.7",
72 | "process": "^0.11.10",
73 | "process-nextick-args": "^2.0.1",
74 | "proj4": "^2.8.0",
75 | "recursive-fs": "^2.1.0",
76 | "round-to": "^5.0.0",
77 | "simple-get": "^4.0.1",
78 | "stream-browserify": "^3.0.0",
79 | "underscore": "^1.13.3",
80 | "vue": "^2.7.14",
81 | "vue-context-menu": "^2.0.6",
82 | "wookmark": "^2.2.0"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/frontend/src/settings.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import {Language} from './model/language';
3 | import Header from '../vue/header.vue';
4 | let langObj;
5 |
6 | async function initRun() {
7 | langObj = await Language.getSingleton();
8 | const t = langObj.t;
9 | const vueSettings = new Vue({
10 | i18n: langObj.vi18n,
11 | async created() {
12 | const self = this;
13 | self.saveFolder = self.saveFolder_ = await window.settings.getSetting('saveFolder'); // eslint-disable-line no-undef
14 | self.lang = self.lang_ = await window.settings.getSetting('lang'); // eslint-disable-line no-undef
15 |
16 | window.settings.on('saveFolderSelected', (event, arg) => { // eslint-disable-line no-undef
17 | self.saveFolder = arg;
18 | });
19 | },
20 | computed: {
21 | dirty() {
22 | return !(this.lang === this.lang_ && this.saveFolder === this.saveFolder_);
23 | }
24 | },
25 | el: '#container',
26 | template: '#settings-vue-template',
27 | components: {
28 | 'header-template': Header
29 | },
30 | data: {
31 | saveFolder: '',
32 | saveFolder_: '',
33 | lang: '',
34 | lang_: ''
35 | },
36 | methods: {
37 | resetSettings() {
38 | this.saveFolder = this.saveFolder_;
39 | this.lang = this.lang_;
40 | },
41 | async saveSettings() {
42 | if (this.saveFolder !== this.saveFolder_) {
43 | await window.settings.setSetting('saveFolder', this.saveFolder); // eslint-disable-line no-undef
44 | this.saveFolder_ = this.saveFolder;
45 | }
46 | if (this.lang !== this.lang_) {
47 | await window.settings.setSetting('lang', this.lang); // eslint-disable-line no-undef
48 | this.lang_ = this.lang;
49 | this.$i18n.i18next.changeLanguage(this.lang);
50 | }
51 | },
52 | async focusSettings(evt) {
53 | evt.target.blur();
54 | window.settings.showSaveFolderDialog(this.saveFolder); // eslint-disable-line no-undef
55 | }
56 | }
57 | });
58 |
59 | let allowClose = false;
60 |
61 | // When move to other pages
62 | const dataNav = document.querySelectorAll('a[data-nav]'); // eslint-disable-line no-undef
63 | for (let i = 0; i < dataNav.length; i++) {
64 | dataNav[i].addEventListener('click', (ev) => {
65 | if (!vueSettings.dirty || confirm(t('settings.confirm_close_no_save'))) { // eslint-disable-line no-undef
66 | allowClose = true;
67 | window.location.href = ev.target.getAttribute('data-nav'); // eslint-disable-line no-undef
68 | }
69 | });
70 | }
71 |
72 | // When application will close
73 | window.addEventListener('beforeunload', (e) => { // eslint-disable-line no-undef
74 | if (!vueSettings.dirty) return;
75 | if (allowClose) {
76 | allowClose = false;
77 | return;
78 | }
79 | e.returnValue = 'false';
80 | setTimeout(() => { // eslint-disable-line no-undef
81 | if (confirm(t('settings.confirm_close_no_save'))) { // eslint-disable-line no-undef
82 | allowClose = true;
83 | window.close(); // eslint-disable-line no-undef
84 | }
85 | }, 2);
86 | });
87 | }
88 |
89 | initRun();
90 |
--------------------------------------------------------------------------------
/html/settings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Maplat Editor
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/css/theme.css:
--------------------------------------------------------------------------------
1 | @import (less) url('../node_modules/bootstrap3/dist/css/bootstrap.css');
2 | @import (less) url('non-responsive.css');
3 | @import (less) url('../node_modules/bootstrap3/dist/css/bootstrap-theme.css');
4 | @import (less) url('../node_modules/ol/ol.css');
5 | @import (less) url('../node_modules/ol-geocoder/dist/ol-geocoder.css');
6 | @import (less) url('../node_modules/ol-contextmenu/dist/ol-contextmenu.css');
7 | @import (less) url('../node_modules/ol-layerswitcher/src/ol-layerswitcher.css');
8 |
9 | html {
10 | height: 100vh;
11 | }
12 |
13 | body {
14 | padding-top: 70px;
15 | padding-bottom: 30px;
16 | height: 100%;
17 | }
18 |
19 | body.settings {
20 | padding-top: 10px;
21 | padding-bottom: 10px;
22 | }
23 |
24 | .container {
25 | width: auto;
26 | }
27 |
28 | .container.main {
29 | height: 100%; /*calc(100vh - 71px );*/
30 | }
31 |
32 | /**
33 | * Grid container
34 | */
35 | .tiles-wrap {
36 | position: relative; /** Needed to ensure items are laid out relative to this container **/
37 | margin: 10px 0;
38 | padding: 0;
39 | list-style-type: none;
40 | }
41 |
42 | /**
43 | * Grid items
44 | */
45 | .tiles-wrap li {
46 | display: block;
47 | opacity: 0;
48 | text-align: center;
49 | list-style-type: none;
50 | background-color: #fff;
51 | float: left;
52 | cursor: pointer;
53 | width: 200px;
54 | padding: 4px;
55 | border: 1px solid #dedede;
56 | -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
57 | -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
58 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
59 | -webkit-border-radius: 2px;
60 | -moz-border-radius: 2px;
61 | border-radius: 2px;
62 | }
63 | .tiles-wrap.wookmark-initialised.animated li {
64 | -webkit-transition: all 0.3s ease-out;
65 | -moz-transition: all 0.3s ease-out;
66 | -o-transition: all 0.3s ease-out;
67 | transition: all 0.3s ease-out;
68 | }
69 |
70 | .tiles-wrap.wookmark-initialised li {
71 | opacity: 1;
72 | }
73 |
74 | .tiles-wrap li.wookmark-inactive {
75 | visibility: hidden;
76 | opacity: 0;
77 | }
78 |
79 | .tiles-wrap li:hover {
80 | background-color: #fafafa;
81 | }
82 |
83 | .tiles-wrap img {
84 | display: block;
85 | }
86 |
87 | .tiles-wrap a {
88 | color: #555;
89 | text-align: center;
90 | /* display: table-cell; */
91 | width: 200px;
92 | height: 200px;
93 | font-size: 2em;
94 | font-weight: bold;
95 | text-decoration: none;
96 | }
97 |
98 | .theme-dropdown .dropdown-menu {
99 | position: static;
100 | display: block;
101 | margin-bottom: 20px;
102 | }
103 |
104 | .h100 {
105 | height: 100%;
106 | }
107 |
108 | .w100 {
109 | width: 100%;
110 | }
111 |
112 | .map-control-header {
113 | padding-top: 10px;
114 | padding-bottom: 10px;
115 | }
116 |
117 | .map-row {
118 | height: calc(100% - 115px);
119 | }
120 |
121 | .map-control-footer {
122 | padding-top: 10px;
123 | }
124 |
125 | .tab-content {
126 | height: calc(100% - 82px);
127 | }
128 |
129 | .auto {
130 | height: auto;
131 | }
132 |
133 | .title-container {
134 | height: 50px;
135 | }
136 |
137 | .title-container h4 {
138 | margin-top: 0px;
139 | }
140 |
141 | .nav-tabs>li>a {
142 | padding-top: 5px;
143 | padding-bottom: 5px;
144 | }
145 |
146 | li.disabled {
147 | pointer-events: none;
148 | }
149 |
150 | .homeBtn {
151 | position: absolute;
152 | right: 20px;
153 | bottom: 35px;
154 | z-index: 1000;
155 | }
156 |
157 | .layer-switcher.shown {
158 | max-height: 300px;
159 | }
--------------------------------------------------------------------------------
/backend/lib/nedb_accessor.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Datastore = require("@seald-io/nedb"); // eslint-disable-line no-undef
4 |
5 | let instance;
6 |
7 | class nedbAccessor {
8 | static getInstance(file) {
9 | if (!instance || instance.file !== file) {
10 | instance = new nedbAccessor(file);
11 | }
12 | return instance;
13 | }
14 |
15 | constructor(file) {
16 | this.file = file;
17 | this.db = new Datastore({ filename: file, autoload: true });
18 | }
19 |
20 | async delete(mapID) {
21 | return new Promise((res, rej) => {
22 | this.db.remove({ _id: mapID }, {}, (err, _num) => {
23 | if (err) rej(err);
24 | else res();
25 | });
26 | });
27 | }
28 |
29 | async find(mapID) {
30 | return new Promise((res, rej) => {
31 | this.db.findOne({ _id: mapID }, (err, doc) => {
32 | if (err) rej(err);
33 | else res(doc);
34 | });
35 | });
36 | }
37 |
38 | async upsert(mapID, data) {
39 | return new Promise((res, rej) => {
40 | data._id = mapID;
41 | this.db.update({ _id: mapID }, data, { upsert: true }, (err, _num) => {
42 | if (err) rej(err);
43 | else res();
44 | });
45 | });
46 | }
47 |
48 | async search(condition = null, skip = 0, limit = 20) {
49 | const where = {};
50 | if (condition) where["$where"] = function() {
51 | return ["title", "officialTitle", "description"].reduce((ret, attr) => {
52 | return ret || checkLocaleAttr(this[attr], condition);
53 | }, false);
54 | };
55 | const task = this.db.find(where).sort({ _id: 1 }).skip(skip).limit(limit + 1);
56 |
57 | return new Promise((res, rej) => {
58 | task.exec((err, docs) => {
59 | if (err) rej(err);
60 | else res(docs);
61 | });
62 | }).then((docs) => {
63 | let next = false;
64 | if (docs.length > limit) {
65 | docs.pop();
66 | next = true;
67 | }
68 | return {
69 | prev: skip > 0,
70 | next,
71 | docs
72 | }
73 | });
74 | }
75 |
76 | async searchExtent(extent) {
77 | const where = {};
78 | where["$where"] = function() {
79 | if (!this.compiled) return false;
80 | const map_extent = this.compiled.vertices_points.reduce((ret, vertex) => {
81 | const merc = vertex[1];
82 | if (ret.length === 0) {
83 | ret = [merc[0], merc[1], merc[0], merc[1]];
84 | } else {
85 | if (ret[0] > merc[0]) ret[0] = merc[0];
86 | if (ret[1] > merc[1]) ret[1] = merc[1];
87 | if (ret[2] < merc[0]) ret[2] = merc[0];
88 | if (ret[3] < merc[1]) ret[3] = merc[1];
89 | }
90 | return ret;
91 | }, []);
92 | return (extent[0] <= map_extent[2] && map_extent[0] <= extent[2] && extent[1] <= map_extent[3] && map_extent[1] <= extent[3]);
93 | };
94 | const task = this.db.find(where).sort({ _id: 1 });
95 |
96 | return new Promise((res, rej) => {
97 | task.exec((err, docs) => {
98 | if (err) rej(err);
99 | else res(docs);
100 | });
101 | });
102 | }
103 | }
104 |
105 | function checkLocaleAttr(attr, condition) {
106 | const conds = condition.trim().split(" ");
107 | const isString = typeof attr === "string";
108 | return conds.reduce((ret, cond) => {
109 | const reg = new RegExp(cond);
110 | if (isString) return ret && (!!attr.match(reg));
111 | else return ret && (!!Object.keys(attr).reduce((ret_, lang) => ret_ || attr[lang].match(reg), false));
112 | }, true);
113 | }
114 |
115 | module.exports = nedbAccessor; // eslint-disable-line no-undef
--------------------------------------------------------------------------------
/html/maplist.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Maplat Editor
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
57 |
58 |
59 |
60 |
61 |
62 |
65 |
66 |
67 |
68 |
{{progressText}}
69 |
70 |
71 |
72 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/backend/src/dataupload.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const path = require('path'); // eslint-disable-line no-undef
4 | const app = require('electron').app; // eslint-disable-line no-undef
5 | const fs = require('fs-extra'); // eslint-disable-line no-undef
6 | const {ipcMain, BrowserWindow} = require("electron"); // eslint-disable-line no-undef
7 | const settings = require('./settings').init(); // eslint-disable-line no-undef
8 |
9 | const AdmZip = require("adm-zip"); // eslint-disable-line no-undef
10 | const nedbAccessor = require("../lib/nedb_accessor"); // eslint-disable-line no-undef
11 | const {exists, normalizeRequestData} = require("../lib/utils"); // eslint-disable-line no-undef
12 |
13 | let mapFolder;
14 | let tileFolder;
15 | let uiThumbnailFolder;
16 | let tmpFolder;
17 | let focused;
18 | let dbFile;
19 | let nedb;
20 |
21 | let initialized = false;
22 |
23 | const DataUpload = {
24 | init() {
25 | const saveFolder = settings.getSetting("saveFolder");
26 | mapFolder = path.resolve(saveFolder, "maps");
27 | fs.ensureDir(mapFolder, () => {
28 | });
29 | tileFolder = path.resolve(saveFolder, "tiles");
30 | fs.ensureDir(tileFolder, () => {
31 | });
32 | uiThumbnailFolder = path.resolve(saveFolder, "tmbs");
33 | fs.ensureDir(uiThumbnailFolder, () => {
34 | });
35 | tmpFolder = settings.getSetting("tmpFolder");
36 |
37 | dbFile = path.resolve(saveFolder, "nedb.db");
38 | nedb = nedbAccessor.getInstance(dbFile);
39 |
40 | focused = BrowserWindow.getFocusedWindow();
41 |
42 | if (!initialized) {
43 | initialized = true;
44 | ipcMain.on('dataupload_showDataSelectDialog', async (event, mapImageRepl) => {
45 | this.showDataSelectDialog(event, mapImageRepl);
46 | });
47 | }
48 | },
49 | showDataSelectDialog(ev, mapImageRepl) {
50 | const dialog = require('electron').dialog; // eslint-disable-line no-undef
51 | const self = this;
52 | dialog.showOpenDialog({ defaultPath: app.getPath('documents'), properties: ['openFile'],
53 | filters: [ {name: mapImageRepl, extensions: ['zip']} ]}).then(async (ret) => {
54 | if (ret.canceled) {
55 | ev.reply('dataupload_uploadedData', {
56 | err: 'Canceled'
57 | });
58 | } else {
59 | self.extractZip(ev, ret.filePaths[0]);
60 | }
61 | });
62 | },
63 | async extractZip(ev, zipFile) {
64 | try {
65 | const dataTmpFolder = path.resolve(tmpFolder, "zip");
66 | await fs.remove(dataTmpFolder);
67 | await fs.ensureDir(dataTmpFolder);
68 | const zip = new AdmZip(zipFile);
69 | zip.extractAllTo(dataTmpFolder, true);
70 | const mapTmpFolder = path.resolve(dataTmpFolder, "maps");
71 | const tileTmpFolder = path.resolve(dataTmpFolder, "tiles");
72 | const tmbTmpFolder = path.resolve(dataTmpFolder, "tmbs");
73 | const mapFile = (await fs.readdir(mapTmpFolder))[0];
74 | const mapID = mapFile.split(/\./)[0];
75 | const mapPath = path.resolve(mapTmpFolder, mapFile);
76 | const mapData = await fs.readJSON(mapPath, "utf8");
77 | const tilePath = path.resolve(tileTmpFolder, mapID);
78 | const tmbPath = path.resolve(tmbTmpFolder, `${mapID}.jpg`);
79 | //const originPath = path.resolve(tmbTmpFolder, mapID);
80 | const tileToPath = path.resolve(tileFolder, mapID);
81 | const tmbToPath = path.resolve(uiThumbnailFolder, `${mapID}.jpg`);
82 |
83 | const existCheckID = await nedb.find(mapID);
84 | if (existCheckID) throw 'Exist';
85 | const existCheckTile = await exists(tilePath);
86 | if (!existCheckTile) throw 'NoTile';
87 | const existCheckTmb = await exists(tmbPath);
88 | if (!existCheckTmb) throw 'NoTmb';
89 |
90 | await nedb.upsert(mapID, mapData);
91 | await fs.remove(tileToPath);
92 | await fs.move(tilePath, tileToPath);
93 | await fs.remove(tmbToPath);
94 | await fs.move(tmbPath, tmbToPath);
95 |
96 | const res = await normalizeRequestData(mapData, `${tileFolder}${path.sep}${mapID}${path.sep}0${path.sep}0`);
97 | res[0].mapID = mapID;
98 | res[0].status = 'Update';
99 | res[0].onlyOne = true;
100 | ev.reply('dataupload_uploadedData', res);
101 | } catch(err) {
102 | if (focused) {
103 | ev.reply('dataupload_uploadedData', {
104 | err
105 | });
106 | } else {
107 | console.log(err); // eslint-disable-line no-undef
108 | }
109 | }
110 | }
111 | };
112 |
113 | module.exports = DataUpload; // eslint-disable-line no-undef
--------------------------------------------------------------------------------
/css/non-responsive.css:
--------------------------------------------------------------------------------
1 | /* 非レスポンシブの再定義
2 | *
3 | * 次のCSSを使用して、コンテナ、ナビゲーションバーのレスポンシブを無効にする
4 | */
5 |
6 | /* .containerをリセット */
7 | .container {
8 | width: 970px;
9 | max-width: none !important;
10 | }
11 |
12 | /* 常にナビゲーションバーを左寄せ */
13 | .navbar-header {
14 | float: left;
15 | }
16 |
17 | /* 折りたたみ中のナビゲーションバーを元に戻す */
18 | .navbar-collapse {
19 | display: block !important;
20 | height: auto !important;
21 | padding-bottom: 0;
22 | overflow: visible !important;
23 | visibility: visible !important;
24 | }
25 |
26 | .navbar-toggle {
27 | display: none;
28 | }
29 | .navbar-collapse {
30 | border-top: 0;
31 | }
32 |
33 | .navbar-brand {
34 | margin-left: -15px;
35 | }
36 |
37 | /* ナビゲーションバーを常に左寄せに適用 */
38 | .navbar-nav {
39 | float: left;
40 | margin: 0;
41 | }
42 | .navbar-nav > li {
43 | float: left;
44 | }
45 | .navbar-nav > li > a {
46 | padding: 15px;
47 | }
48 |
49 | /* 上記でfloatを再定義したので右寄せ用に再設定 */
50 | .navbar-nav.navbar-right {
51 | float: right;
52 | }
53 |
54 | /* カスタムのドロップダウンを元に戻す */
55 | .navbar .navbar-nav .open .dropdown-menu {
56 | position: absolute;
57 | float: left;
58 | background-color: #fff;
59 | border: 1px solid #ccc;
60 | border: 1px solid rgba(0, 0, 0, .15);
61 | border-width: 0 1px 1px;
62 | border-radius: 0 0 4px 4px;
63 | -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
64 | box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
65 | }
66 | .navbar-default .navbar-nav .open .dropdown-menu > li > a {
67 | color: #333;
68 | }
69 | .navbar .navbar-nav .open .dropdown-menu > li > a:hover,
70 | .navbar .navbar-nav .open .dropdown-menu > li > a:focus,
71 | .navbar .navbar-nav .open .dropdown-menu > .active > a,
72 | .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
73 | .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
74 | color: #fff !important;
75 | background-color: #428bca !important;
76 | }
77 | .navbar .navbar-nav .open .dropdown-menu > .disabled > a,
78 | .navbar .navbar-nav .open .dropdown-menu > .disabled > a:hover,
79 | .navbar .navbar-nav .open .dropdown-menu > .disabled > a:focus {
80 | color: #999 !important;
81 | background-color: transparent !important;
82 | }
83 |
84 | /* フォームの展開を元に戻す */
85 | .navbar-form {
86 | float: left;
87 | width: auto;
88 | padding-top: 0;
89 | padding-bottom: 0;
90 | margin-right: 0;
91 | margin-left: 0;
92 | border: 0;
93 | -webkit-box-shadow: none;
94 | box-shadow: none;
95 | }
96 |
97 | /* .form-inlineスタイルをミックスインするので、forms.lessからコピーして貼り付け */
98 | .navbar-form .form-group {
99 | display: inline-block;
100 | margin-bottom: 0;
101 | vertical-align: middle;
102 | }
103 |
104 | .navbar-form .form-control {
105 | display: inline-block;
106 | width: auto;
107 | vertical-align: middle;
108 | }
109 |
110 | .navbar-form .form-control-static {
111 | display: inline-block;
112 | }
113 |
114 | .navbar-form .input-group {
115 | display: inline-table;
116 | vertical-align: middle;
117 | }
118 |
119 | .navbar-form .input-group .input-group-addon,
120 | .navbar-form .input-group .input-group-btn,
121 | .navbar-form .input-group .form-control {
122 | width: auto;
123 | }
124 |
125 | .navbar-form .input-group > .form-control {
126 | width: 100%;
127 | }
128 |
129 | .navbar-form .control-label {
130 | margin-bottom: 0;
131 | vertical-align: middle;
132 | }
133 |
134 | .navbar-form .radio,
135 | .navbar-form .checkbox {
136 | display: inline-block;
137 | margin-top: 0;
138 | margin-bottom: 0;
139 | vertical-align: middle;
140 | }
141 |
142 | .navbar-form .radio label,
143 | .navbar-form .checkbox label {
144 | padding-left: 0;
145 | }
146 |
147 | .navbar-form .radio input[type="radio"],
148 | .navbar-form .checkbox input[type="checkbox"] {
149 | position: relative;
150 | margin-left: 0;
151 | }
152 |
153 | .navbar-form .has-feedback .form-control-feedback {
154 | top: 0;
155 | }
156 |
157 | /* 小サイズ画面での横並びフォームの圧縮の取消 */
158 | .form-inline .form-group {
159 | display: inline-block;
160 | margin-bottom: 0;
161 | vertical-align: middle;
162 | }
163 |
164 | .form-inline .form-control {
165 | display: inline-block;
166 | width: auto;
167 | vertical-align: middle;
168 | }
169 |
170 | .form-inline .form-control-static {
171 | display: inline-block;
172 | }
173 |
174 | .form-inline .input-group {
175 | display: inline-table;
176 | vertical-align: middle;
177 | }
178 | .form-inline .input-group .input-group-addon,
179 | .form-inline .input-group .input-group-btn,
180 | .form-inline .input-group .form-control {
181 | width: auto;
182 | }
183 |
184 | .form-inline .input-group > .form-control {
185 | width: 100%;
186 | }
187 |
188 | .form-inline .control-label {
189 | margin-bottom: 0;
190 | vertical-align: middle;
191 | }
192 |
193 | .form-inline .radio,
194 | .form-inline .checkbox {
195 | display: inline-block;
196 | margin-top: 0;
197 | margin-bottom: 0;
198 | vertical-align: middle;
199 | }
200 | .form-inline .radio label,
201 | .form-inline .checkbox label {
202 | padding-left: 0;
203 | }
204 |
205 | .form-inline .radio input[type="radio"],
206 | .form-inline .checkbox input[type="checkbox"] {
207 | position: relative;
208 | margin-left: 0;
209 | }
210 |
211 | .form-inline .has-feedback .form-control-feedback {
212 | top: 0;
213 | }
214 |
--------------------------------------------------------------------------------
/backend/src/mapupload.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {Jimp} = require('../lib/utils'); // eslint-disable-line no-undef
4 |
5 | const path = require('path'); // eslint-disable-line no-undef
6 | const app = require('electron').app; // eslint-disable-line no-undef
7 | const fs = require('fs-extra'); // eslint-disable-line no-undef
8 | const {ipcMain} = require("electron"); // eslint-disable-line no-undef
9 | const settings = require('./settings').init(); // eslint-disable-line no-undef
10 |
11 | const fileUrl = require('file-url'); // eslint-disable-line no-undef
12 | const thumbExtractor = require('../lib/ui_thumbnail'); // eslint-disable-line no-undef
13 | const ProgressReporter = require('../lib/progress_reporter'); // eslint-disable-line no-undef
14 |
15 | let mapFolder;
16 | let tileFolder;
17 | let uiThumbnailFolder;
18 | let tmpFolder;
19 | let outFolder;
20 | let extKey;
21 | let toExtKey;
22 |
23 | let initialized = false;
24 |
25 | const MapUpload = {
26 | init() {
27 | const saveFolder = settings.getSetting('saveFolder');
28 | mapFolder = path.resolve(saveFolder, "maps");
29 | fs.ensureDir(mapFolder, () => {
30 | });
31 | tileFolder = path.resolve(saveFolder, "tiles");
32 | fs.ensureDir(tileFolder, () => {
33 | });
34 | uiThumbnailFolder = path.resolve(saveFolder, "tmbs");
35 | fs.ensureDir(uiThumbnailFolder, () => {
36 | });
37 | tmpFolder = settings.getSetting('tmpFolder');
38 |
39 | if (!initialized) {
40 | initialized = true;
41 | ipcMain.on('mapupload_showMapSelectDialog', async (event, mapImageRepl) => {
42 | this.showMapSelectDialog(event, mapImageRepl);
43 | });
44 | }
45 | },
46 | showMapSelectDialog(ev, mapImageRepl) {
47 | const dialog = require('electron').dialog; // eslint-disable-line no-undef
48 | const self = this;
49 | dialog.showOpenDialog({ defaultPath: app.getPath('documents'), properties: ['openFile'],
50 | filters: [ {name: mapImageRepl, extensions: ['jpg', 'png', 'jpeg']} ]}).then((ret) => {
51 | if (ret.canceled) {
52 | ev.reply('mapupload_uploadedMap', {
53 | err: 'Canceled'
54 | });
55 | } else {
56 | self.imageCutter(ev, ret.filePaths[0]);
57 | }
58 | });
59 | },
60 | async imageCutter(ev, srcFile) {
61 | try {
62 | const regex = new RegExp('([^\\/]+)\\.([^\\.]+)$');
63 | await new Promise((resolve, reject) => {
64 | if (srcFile.match(regex)) {
65 | extKey = RegExp.$2;
66 | toExtKey = extKey.toLowerCase();
67 | if (toExtKey === 'jpeg') toExtKey = "jpg";
68 | } else {
69 | reject('画像拡張子エラー');
70 | }
71 | outFolder = `${tmpFolder}${path.sep}tiles`;
72 | try {
73 | fs.statSync(outFolder);
74 | fs.remove(outFolder, (err) => {
75 | if (err) {
76 | reject(err);
77 | return;
78 | }
79 | resolve();
80 | });
81 | } catch(err) {
82 | resolve();
83 | }
84 | });
85 | await fs.ensureDir(outFolder);
86 | const imageJimp = await Jimp.read(srcFile);
87 | const width = imageJimp.bitmap.width;
88 | const height = imageJimp.bitmap.height;
89 | const maxZoom = Math.ceil(Math.log(Math.max(width, height) / 256)/ Math.log(2));
90 |
91 | const tasks = [];
92 | for (let z = maxZoom; z >= 0; z--) {
93 | const pw = Math.round(width / Math.pow(2, maxZoom - z));
94 | const ph = Math.round(height / Math.pow(2, maxZoom - z));
95 | for (let tx = 0; tx * 256 < pw; tx++) {
96 | const tw = (tx + 1) * 256 > pw ? pw - tx * 256 : 256;
97 | const sx = tx * 256 * Math.pow(2, maxZoom - z);
98 | const sw = (tx + 1) * 256 * Math.pow(2, maxZoom - z) > width ? width - sx : 256 * Math.pow(2, maxZoom - z);
99 | const tileFolder = path.resolve(outFolder, `${z}`, `${tx}`);
100 | await fs.ensureDir(tileFolder);
101 | for (let ty = 0; ty * 256 < ph; ty++) {
102 | const th = (ty + 1) * 256 > ph ? ph - ty * 256 : 256;
103 | const sy = ty * 256 * Math.pow(2, maxZoom - z);
104 | const sh = (ty + 1) * 256 * Math.pow(2, maxZoom - z) > height ? height - sy : 256 * Math.pow(2, maxZoom - z);
105 |
106 | const tileFile = path.resolve(tileFolder, `${ty}.${toExtKey}`);
107 | tasks.push([tileFile, sx, sy, sw, sh, tw, th]);
108 | }
109 | }
110 | }
111 |
112 | const progress = new ProgressReporter("mapedit", tasks.length, 'mapupload.dividing_tile', 'mapupload.next_thumbnail');
113 | progress.update(ev, 0);
114 |
115 | for (let i = 0; i < tasks.length; i++) {
116 | const task = tasks[i];
117 |
118 | const canvasJimp = imageJimp.clone().crop(task[1], task[2], task[3], task[4]).resize(task[5], task[6]);
119 | await canvasJimp.write(task[0]);
120 |
121 | progress.update(ev, i + 1);
122 | await new Promise((s) => setTimeout(s, 1)); // eslint-disable-line no-undef
123 | }
124 |
125 | await fs.copy(srcFile, path.resolve(outFolder, `original.${toExtKey}`));
126 |
127 | const thumbFrom = path.resolve(outFolder, "0", "0", `0.${toExtKey}`);
128 | const thumbTo = path.resolve(outFolder, "thumbnail.jpg");
129 | await thumbExtractor.make_thumbnail(thumbFrom, thumbTo);
130 |
131 | const url = `${fileUrl(outFolder)}/{z}/{x}/{y}.${toExtKey}`;
132 | ev.reply('mapupload_uploadedMap', {
133 | width,
134 | height,
135 | url,
136 | imageExtension: toExtKey
137 | });
138 | } catch(err) {
139 | ev.reply('mapupload_uploadedMap', {
140 | err
141 | });
142 | }
143 | }
144 | };
145 |
146 | module.exports = MapUpload; // eslint-disable-line no-undef
--------------------------------------------------------------------------------
/frontend/src/maplist.js:
--------------------------------------------------------------------------------
1 | import Wookmark from 'wookmark/wookmark';
2 | import Vue from 'vue';
3 | import {Language} from './model/language';
4 | import Header from '../vue/header.vue';
5 | import VueContextMenu from "vue-context-menu";
6 | import bsn from "bootstrap.native";
7 |
8 | let langObj;
9 | const newMenuData = () => ({ mapID: "", name: "" });
10 |
11 | let vueModal; // eslint-disable-line prefer-const
12 |
13 | async function initRun() {
14 | await window.baseApi.require('maplist'); // eslint-disable-line no-undef
15 | langObj = await Language.getSingleton();
16 | new Vue({
17 | i18n: langObj.vi18n,
18 | watch: {
19 | condition() {
20 | this.search();
21 | }
22 | },
23 | mounted() {
24 | const t = langObj.t;
25 | window.maplist.start(); // eslint-disable-line no-undef
26 |
27 | window.addEventListener('resize', this.handleResize); // eslint-disable-line no-undef
28 |
29 | window.maplist.on("migrationConfirm", () => { // eslint-disable-line no-undef
30 | if (confirm(t("maplist.migration_confirm"))) { // eslint-disable-line no-undef
31 | vueModal.show(t("maplist.migrating"));
32 | setTimeout(() => { // eslint-disable-line no-undef
33 | window.maplist.migration(); // eslint-disable-line no-undef
34 | }, 1000);
35 | } else {
36 | window.maplist.request(); // eslint-disable-line no-undef
37 | }
38 | });
39 | window.maplist.on("deleteOldConfirm", () => { // eslint-disable-line no-undef
40 | vueModal.hide();
41 | setTimeout(() => { // eslint-disable-line no-undef
42 | if (confirm(t("maplist.delete_old_confirm"))) { // eslint-disable-line no-undef
43 | vueModal.show(t("maplist.deleting_old"));
44 | setTimeout(() => { // eslint-disable-line no-undef
45 | window.maplist.deleteOld(); // eslint-disable-line no-undef
46 | }, 1000);
47 | }
48 | }, 1000);
49 | });
50 | window.maplist.on("deletedOld", () => { // eslint-disable-line no-undef
51 | const t = langObj.t;
52 | vueModal.finish(t('maplist.deleted_old'));
53 | });
54 | window.maplist.on("deleteError", () => { // eslint-disable-line no-undef
55 | const t = langObj.t;
56 | alert(t('maplist.delete_error')); // eslint-disable-line no-undef
57 | });
58 | window.maplist.on('taskProgress', (ev, args) => { // eslint-disable-line no-undef
59 | const t = langObj.t;
60 | vueModal.progress(t(args.text), args.percent, args.progress);
61 | });
62 | window.maplist.on('mapList', (ev, args) => { // eslint-disable-line no-undef
63 | this.maplist = [];
64 | this.prev = args.prev;
65 | this.next = args.next;
66 | if (args.pageUpdate) {
67 | this.page = args.pageUpdate;
68 | }
69 | args.docs.forEach((doc) => {
70 | const map = {
71 | mapID: doc.mapID,
72 | name: doc.title,
73 | };
74 | if (!doc.width || !doc.height || !doc.thumbnail) {
75 | map.width = 190;
76 | map.height = 190;
77 | map.image = '../img/no_image.png';
78 | } else {
79 | map.width = doc.width > doc.height ? 190 : Math.floor(190 / doc.height * doc.width);
80 | map.height = doc.width > doc.height ? Math.floor(190 / doc.width * doc.height) : 190;
81 | map.image = doc.thumbnail;
82 | }
83 |
84 | this.maplist.push(map);
85 | });
86 |
87 | Vue.nextTick(() => {
88 | new Wookmark('#maplist');
89 | this.handleResize();
90 | });
91 | });
92 | Vue.nextTick(() => {
93 | new Wookmark('#maplist');
94 | this.handleResize();
95 | });
96 | },
97 | el: '#container',
98 | template: "#maplist-vue-template",
99 | components: {
100 | "header-template": Header,
101 | "context-menu": VueContextMenu
102 | },
103 | data() {
104 | const size = calcResize(document.body.clientWidth); // eslint-disable-line no-undef
105 | return {
106 | maplist: [],
107 | padding: size[0],
108 | searchWidth: size[1],
109 | prev: false,
110 | next: false,
111 | page: 1,
112 | condition: "",
113 | menuData: newMenuData(),
114 | showCtx: false,
115 | contextClicks: []
116 | }
117 | },
118 | methods: {
119 | handleResize() {
120 | const size = calcResize(document.body.clientWidth); // eslint-disable-line no-undef
121 | this.padding = size[0];
122 | this.searchWidth = size[1];
123 | },
124 | prevSearch() {
125 | this.page--;
126 | this.search();
127 | },
128 | nextSearch() {
129 | this.page++;
130 | this.search();
131 | },
132 | search() {
133 | window.maplist.request(this.condition, this.page); // eslint-disable-line no-undef
134 | },
135 | onCtxOpen(locals) {
136 | this.menuData = locals;
137 | },
138 | onCtxClose(locals) { // eslint-disable-line no-unused-vars
139 | },
140 | resetCtxLocals() {
141 | this.menuData = newMenuData();
142 | },
143 | deleteMap(menuData) {
144 | const t = langObj.t;
145 | if (!confirm(t('maplist.delete_confirm', { name: menuData.name }))) return; // eslint-disable-line no-undef
146 | window.maplist.delete(menuData.mapID, this.condition, this.page); // eslint-disable-line no-undef
147 | }
148 | },
149 | });
150 | }
151 |
152 | vueModal = new Vue({
153 | el: "#modalBody",
154 | data: {
155 | modal: new bsn.Modal(document.getElementById('staticModal'), {}), //eslint-disable-line no-undef
156 | percent: 0,
157 | progressText: '',
158 | enableClose: false,
159 | text: ''
160 | },
161 | methods: {
162 | show(text) {
163 | this.text = text;
164 | this.percent = 0;
165 | this.progressText = '';
166 | this.enableClose = false;
167 | this.modal.show();
168 | },
169 | progress(text, perecent, progress) {
170 | this.text = text;
171 | this.percent = perecent;
172 | this.progressText = progress;
173 | },
174 | finish(text) {
175 | this.text = text;
176 | this.enableClose = true;
177 | },
178 | hide() {
179 | this.modal.hide();
180 | }
181 | }
182 | });
183 |
184 | function calcResize(width) {
185 | const pow = Math.floor((width - 25) / 205);
186 | return [Math.floor((width - 205 * pow + 5) / 2), 205 * (pow - 2) - 5];
187 | }
188 |
189 | initRun();
190 |
--------------------------------------------------------------------------------
/backend/src/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {app, BrowserWindow, ipcMain, Menu} = require('electron'); // eslint-disable-line no-undef
4 | const fs = require('fs-extra'); // eslint-disable-line no-undef
5 | const openAboutWindow =require('about-window').default; // eslint-disable-line no-undef
6 | const Settings = require('./settings'); // eslint-disable-line no-undef
7 | const path = require('path'); // eslint-disable-line no-undef
8 |
9 | let settings;
10 | let menuTemplate;
11 | let mainWindow = null;
12 |
13 | let force_quit = false;
14 | const appWidth = 1200;
15 | const appHeight = 800;
16 |
17 | const isDev = isExistFile('.env');
18 |
19 | const menuList = [
20 | 'menu.quit',
21 | 'menu.about',
22 | 'menu.edit',
23 | 'menu.undo',
24 | 'menu.redo',
25 | 'menu.cut',
26 | 'menu.copy',
27 | 'menu.paste',
28 | 'menu.select_all'
29 | ];
30 |
31 | app.commandLine.appendSwitch('js-flags', '--max-old-space-size=8192');
32 | app.disableHardwareAcceleration();
33 |
34 | app.on('window-all-closed', () => {
35 | if (process.platform != 'darwin') // eslint-disable-line no-undef
36 | app.quit();
37 | });
38 |
39 | // This is another place to handle events after all windows are closed
40 | app.on('will-quit', () => {
41 | // This is a good place to add tests insuring the app is still
42 | // responsive and all windows are closed.
43 | console.log("will-quit"); // eslint-disable-line no-console,no-undef
44 | mainWindow = null;
45 | });
46 |
47 | app.on('ready', async () => {
48 | settings = await Settings.asyncInit();
49 | const menu = setupMenu();
50 | Menu.setApplicationMenu(menu);
51 | settings.on('changeLang', () => {
52 | menulabelChange();
53 | const menu = Menu.buildFromTemplate(menuTemplate);
54 | Menu.setApplicationMenu(menu);
55 | });
56 |
57 | // ブラウザ(Chromium)の起動, 初期画面のロード
58 | mainWindow = new BrowserWindow({
59 | width: appWidth,
60 | height: appHeight,
61 | webPreferences: {
62 | preload: path.join(__dirname, '../../frontend/api/preload.js'), // eslint-disable-line no-undef
63 | sandbox: false
64 | }
65 | });
66 | const indexurl = `file://${__dirname.replace(/\\/g, '/')}/../../html/maplist.html`; // eslint-disable-line no-undef
67 | mainWindow.loadURL(indexurl);
68 | mainWindow.setMinimumSize(appWidth, appHeight);
69 |
70 | ipcMain.on('require', (event, ...args) => {
71 | // Backend logic registration
72 | const module_name = args[0];
73 | const backend = require(`./${module_name}`); // eslint-disable-line no-undef
74 | backend.init();
75 | // Event that backend logic registration was finished
76 | event.reply('require_ready');
77 | });
78 |
79 | // Continue to handle mainWindow "close" event here
80 | mainWindow.on('close', (e) => {
81 | console.log("close"); // eslint-disable-line no-console,no-undef
82 | if(process.platform == 'darwin' && !force_quit){ // eslint-disable-line no-undef
83 | e.preventDefault();
84 | mainWindow.hide();
85 | }
86 | });
87 |
88 | // You can use 'before-quit' instead of (or with) the close event
89 | app.on('before-quit', () => {
90 | // Handle menu-item or keyboard shortcut quit here
91 | console.log("before-quit"); // eslint-disable-line no-console,no-undef
92 | force_quit = true;
93 | });
94 |
95 | app.on('activate', () => {
96 | console.log("reactive"); // eslint-disable-line no-console,no-undef
97 | mainWindow.show();
98 | });
99 | });
100 |
101 | function setupMenu() {
102 | const t = settings.t;
103 | // メニュー情報の作成
104 | menuTemplate = [
105 | {
106 | label: 'MaplatEditor',
107 | submenu: [
108 | {
109 | id: 'menu.quit',
110 | label: t('menu.quit'),
111 | accelerator: 'CmdOrCtrl+Q',
112 | click() {
113 | app.quit();
114 | }
115 | },
116 | {
117 | type: 'separator',
118 | },
119 | {
120 | id: 'menu.about',
121 | label: t('menu.about'),
122 | click() {
123 | openAboutWindow({
124 | icon_path: path.resolve(__dirname, '../../img/icon.png'), // eslint-disable-line no-undef
125 | product_name: 'MaplatEditor',
126 | copyright: 'Copyright (c) 2015-2022 Code for History',
127 | use_version_info: true,
128 | win_options: {
129 | title: settings.t('menu.about')
130 | }
131 | });
132 | }
133 | },
134 | ]
135 | },
136 | {
137 | id: 'menu.edit',
138 | label: t('menu.edit'),
139 | submenu: [
140 | {
141 | id: 'menu.undo',
142 | label: t('menu.undo'),
143 | accelerator: 'CmdOrCtrl+Z',
144 | enabled: false,
145 | click(menuItem, focusedWin) { // eslint-disable-line no-unused-vars
146 | // Undo.
147 | // focusedWin.webContents.undo();
148 |
149 | // Run some custom code.
150 | }
151 | },
152 | {
153 | id: 'menu.redo',
154 | label: t('menu.redo'),
155 | accelerator: 'Shift+CmdOrCtrl+Z',
156 | enabled: false,
157 | click(menuItem, focusedWin) { // eslint-disable-line no-unused-vars
158 | // Undo.
159 | // focusedWin.webContents.undo();
160 |
161 | // Run some custom code.
162 | }
163 | },
164 | { type: "separator" },
165 | {
166 | id: 'menu.cut',
167 | label: t('menu.cut'),
168 | accelerator: 'CmdOrCtrl+X',
169 | selector: 'cut:'
170 | },
171 | {
172 | id: 'menu.copy',
173 | label: t('menu.copy'),
174 | accelerator: 'CmdOrCtrl+C',
175 | selector: 'copy:'
176 | },
177 | {
178 | id: 'menu.paste',
179 | label: t('menu.paste'),
180 | accelerator: 'CmdOrCtrl+V',
181 | selector: 'paste:'
182 | },
183 | {
184 | id: 'menu.select_all',
185 | label: t('menu.select_all'),
186 | accelerator: 'CmdOrCtrl+A',
187 | selector: 'selectAll:'
188 | }
189 | ]
190 | }, /*{
191 |
192 |
193 |
194 | label: 'File',
195 | submenu: [
196 | {label: 'Open', accelerator: 'Command+O', click() {
197 | // 「ファイルを開く」ダイアログの呼び出し
198 | const {dialog} = require('electron'); // eslint-disable-line no-undef
199 | dialog.showOpenDialog({ properties: ['openDirectory']}, (baseDir) => {
200 | if(baseDir && baseDir[0]) {
201 | openWindow(baseDir[0]); // eslint-disable-line no-undef
202 | }
203 | });
204 | }}
205 | ]
206 | }, */
207 | ];
208 |
209 | const devMenu = {
210 | id: 'menu.dev',
211 | label: t('menu.dev'),
212 | submenu: [
213 | {
214 | id: 'menu.reload',
215 | label: t('menu.reload'),
216 | accelerator: 'Command+R',
217 | click() {
218 | BrowserWindow.getFocusedWindow().reload();
219 | }},
220 | {
221 | id: 'menu.tools',
222 | label: t('menu.tools'),
223 | accelerator: 'Alt+Command+I',
224 | click() {
225 | BrowserWindow.getFocusedWindow().toggleDevTools();
226 | }}
227 | ]
228 | };
229 |
230 | if (isDev || 1) { // eslint-disable-line no-constant-condition
231 | menuTemplate.push(devMenu);
232 | menuList.push('menu.dev', 'menu.reload', 'menu.tools');
233 | }
234 |
235 | const menu = Menu.buildFromTemplate(menuTemplate);
236 | return menu;
237 | }
238 |
239 | function menulabelChange(list) {
240 | if (!list) {
241 | list = menuList.reduce((prev, curr) => {
242 | prev[curr] = settings.t(curr);
243 | return prev;
244 | }, {});
245 | }
246 | menuTemplate.map((menu) => {
247 | if (list[menu.id]) menu.label = list[menu.id];
248 | if (menu.submenu) {
249 | menu.submenu.map((submenu) => {
250 | if (list[submenu.id]) submenu.label = list[submenu.id];
251 | });
252 | }
253 | });
254 | }
255 |
256 | function isExistFile(file) {
257 | try {
258 | fs.statSync(file);
259 | return true;
260 | } catch(err) {
261 | if(err.code === 'ENOENT') return false;
262 | }
263 | }
--------------------------------------------------------------------------------
/backend/src/settings.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const EventEmitter = require('events'); // eslint-disable-line no-undef
4 | const storage = require('electron-json-storage'); // eslint-disable-line no-undef
5 | const defaultStoragePath = storage.getDefaultDataPath();
6 | const path = require('path'); // eslint-disable-line no-undef
7 | const app = require('electron').app; // eslint-disable-line no-undef
8 | const fs = require('fs-extra'); // eslint-disable-line no-undef
9 | const tmsListDefault = require('../../tms_list.json'); // eslint-disable-line no-undef
10 | const i18next = require('i18next'); // eslint-disable-line no-undef
11 | const Backend = require('i18next-fs-backend'); // eslint-disable-line no-undef
12 | const {ipcMain} = require("electron"); // eslint-disable-line no-undef
13 | let settings;
14 | let editorStoragePath;
15 |
16 | const protect = [
17 | 'tmpFolder',
18 | 'tmsList'
19 | ];
20 |
21 | const defaultSetting = {
22 | lang: 'ja'
23 | };
24 |
25 | class Settings extends EventEmitter {
26 | static init() {
27 | if (!settings) {
28 | settings = new Settings();
29 | ipcMain.on('settings_lang', async (ev) => {
30 | await settings.asyncReady;
31 | ev.reply('settings_lang_got', settings.json.lang);
32 | });
33 | ipcMain.on('settings_setSetting', async (ev, key, value) => {
34 | await settings.asyncReady;
35 | settings.setSetting(key, value);
36 | ev.reply('settings_setSetting_finished');
37 | });
38 | ipcMain.on('settings_getSetting', async (ev, key) => {
39 | await settings.asyncReady;
40 | const value = key == null ? settings.getSetting() : settings.getSetting(key);
41 | ev.reply('settings_getSetting_finished', value);
42 | });
43 | ipcMain.on('settings_showSaveFolderDialog', async (ev, current) => {
44 | await settings.asyncReady;
45 | settings.showSaveFolderDialog(ev, current);
46 | });
47 | }
48 | return settings;
49 | }
50 |
51 | static asyncInit() {
52 | const settings = Settings.init();
53 | return settings.asyncReady;
54 | }
55 |
56 | constructor() {
57 | super();
58 | this.behaviorChain = [];
59 | this.currentPosition = 0;
60 | let resolveEditorSetting, resolveI18n;
61 | this.asyncReady = Promise.all([
62 | new Promise((resolve) => {
63 | resolveI18n = resolve;
64 | }),
65 | new Promise((resolve) => {
66 | resolveEditorSetting = resolve;
67 | })
68 | ]).then((res) => res[0]);
69 | this.defaultStorage().getAll((error, data) => {
70 | if (error) throw error;
71 |
72 | if (Object.keys(data).length === 0) {
73 | this.json = {
74 | saveFolder: path.resolve(app.getPath('documents') + path.sep + app.getName())
75 | };
76 | this.defaultStorage().set('saveFolder', this.json.saveFolder, {});
77 | } else {
78 | this.json = data;
79 | }
80 | fs.ensureDir(this.json.saveFolder, () => {
81 | editorStoragePath = `${this.json.saveFolder}${path.sep}settings`;
82 | this.editorStorage().get('tmsList', {}, (error, data) => {
83 | if (!Array.isArray(data)) {
84 | data = [];
85 | this.editorStorage().set('tmsList', [], {});
86 | }
87 | this.json.tmsList = tmsListDefault.concat(data);
88 | resolveEditorSetting();
89 | });
90 | });
91 | this.json = Object.assign(defaultSetting, this.json);
92 | this.json.tmpFolder = path.resolve(`${app.getPath('temp')}${path.sep}${app.getName()}`);
93 | fs.ensureDir(this.json.tmpFolder, () => {});
94 |
95 | const lang = this.json.lang;
96 | this.i18n = i18next.use(Backend);
97 | const i18nPromise = this.i18n.init({
98 | lng: lang,
99 | fallbackLng: 'en',
100 | backend: {
101 | loadPath: `${__dirname}/../../locales/{{lng}}/{{ns}}.json` // eslint-disable-line no-undef
102 | }
103 | });
104 | i18nPromise.then((t) => {
105 | this.t = t;
106 | this.translate = (dataFragment) => {
107 | if (!dataFragment || typeof dataFragment != 'object') return dataFragment;
108 | const langs = Object.keys(dataFragment);
109 | let key = langs.reduce((prev, curr) => {
110 | if (curr === 'en' || !prev) {
111 | prev = dataFragment[curr];
112 | }
113 | return prev;
114 | }, null);
115 | key = (typeof key == 'string') ? key : `${key}`;
116 | if (this.i18n.exists(key, {ns: 'translation', nsSeparator: '__X__yX__X__'}))
117 | return this.t(key, {ns: 'translation', nsSeparator: '__X__yX__X__'});
118 | for (let i = 0; i < langs.length; i++) {
119 | const lang = langs[i];
120 | this.i18n.addResource(lang, 'translation', key, dataFragment[lang]);
121 | }
122 | return this.t(key, {ns: 'translation', nsSeparator: '__X__yX__X__'});
123 | };
124 | resolveI18n(this);
125 | });
126 | });
127 | }
128 |
129 | defaultStorage() {
130 | storage.setDataPath(defaultStoragePath);
131 | return storage;
132 | }
133 |
134 | editorStorage() {
135 | storage.setDataPath(editorStoragePath);
136 | return storage;
137 | }
138 |
139 | do(verb, data) {
140 | if (this.redoable) {
141 | this.behaviorChain = this.behaviorChain.slice(0, this.currentPosition + 1);
142 | }
143 | this.behaviorChain.push({verb, data});
144 | this.currentPosition++;
145 | }
146 |
147 | get redoable() {
148 | return this.behaviorChain.length !== this.currentPosition;
149 | }
150 |
151 | redo() {
152 | if (!this.redoable) return;
153 | this.currentPosition++;
154 | return this.behaviorChain[this.currentPosition].data;
155 | }
156 |
157 | get undoable() {
158 | return this.currentPosition !== 0;
159 | }
160 |
161 | undo() {
162 | if (!this.undoable) return;
163 | this.currentPosition--;
164 | return this.behaviorChain[this.currentPosition].data;
165 | }
166 |
167 | get menuData() {
168 | return {
169 | undoable: this.undoable,
170 | redoable: this.redoable,
171 | undo: this.undoable ? this.behaviorChain[this.currentPosition - 1].verb : undefined,
172 | redo: this.redoable ? this.behaviorChain[this.currentPosition].verb : undefined
173 | };
174 | }
175 |
176 | async getTmsListOfMapID(mapID) {
177 | return new Promise((resolve) => {
178 | const settingKey = `tmsList.${mapID}`;
179 | const tmsListBase = this.json.tmsList;
180 | this.editorStorage().get(settingKey, {}, (error, data) => {
181 | let saveFlag = false;
182 | const tmsList = [];
183 | tmsListBase.map((tms) => {
184 | if (tms.always) {
185 | tmsList.push(tms);
186 | return;
187 | }
188 | const mapID = tms.mapID;
189 | let flag = data[mapID];
190 | if (flag == null) {
191 | flag = data[mapID] = true;
192 | saveFlag = true;
193 | }
194 | if (flag) {
195 | tmsList.push(tms);
196 | }
197 | });
198 | if (saveFlag) {
199 | this.editorStorage().set(settingKey, data, {}, () => {
200 | resolve(tmsList);
201 | });
202 | } else resolve(tmsList);
203 | });
204 | });
205 | }
206 |
207 | getSetting(key) {
208 | return this.json[key];
209 | }
210 |
211 | getSettings() {
212 | return this.json;
213 | }
214 |
215 | setSetting(key, value) {
216 | if (protect.indexOf(key) >= 0) throw `"${key}" is protected.`;
217 | this.json[key] = value;
218 | this.defaultStorage().set(key, value, {}, (error) => {
219 | if (error) throw error;
220 | if (key === 'lang') {
221 | this.i18n.changeLanguage(value, () => {
222 | this.emit('changeLang');
223 | });
224 | } else if (key === 'saveFolder') {
225 | fs.ensureDir(value, () => {
226 | editorStoragePath = `${value}${path.sep}settings`;
227 | });
228 | }
229 | });
230 | }
231 |
232 | showSaveFolderDialog(ev, oldSetting) {
233 | const dialog = require('electron').dialog; // eslint-disable-line no-undef
234 | dialog.showOpenDialog({ defaultPath: oldSetting, properties: ['openDirectory']}).then((ret) => {
235 | if(!ret.canceled) {
236 | ev.reply('settings_saveFolderSelected', ret.filePaths[0]);
237 | }
238 | });
239 | }
240 | }
241 | Settings.init();
242 |
243 | module.exports = Settings; // eslint-disable-line no-undef
244 |
--------------------------------------------------------------------------------
/backend/src/wmts_generator.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {Jimp} = require('../lib/utils'); // eslint-disable-line no-undef
4 |
5 | const Tin = require('@maplat/tin').default; // eslint-disable-line no-undef
6 |
7 | const path = require('path'); // eslint-disable-line no-undef
8 | //const app = require('electron').app; // eslint-disable-line no-undef
9 | const fs = require('fs-extra'); // eslint-disable-line no-undef
10 | const {ipcMain} = require("electron"); // eslint-disable-line no-undef
11 | const settings = require('./settings').init(); // eslint-disable-line no-undef
12 |
13 | const MERC_MAX = 20037508.342789244;
14 | const ProgressReporter = require('../lib/progress_reporter'); // eslint-disable-line no-undef
15 |
16 | let mapFolder;
17 | let wmtsFolder;
18 | let originalFolder;
19 | let tmpFolder; // eslint-disable-line no-unused-vars
20 |
21 | let initialized = false;
22 |
23 | const WmtsGenerator = {
24 | init() {
25 | const saveFolder = settings.getSetting('saveFolder');
26 | mapFolder = path.resolve(saveFolder, "maps");
27 | fs.ensureDir(mapFolder, () => {
28 | });
29 | wmtsFolder = path.resolve(saveFolder, "wmts");
30 | fs.ensureDir(wmtsFolder, () => {
31 | });
32 | originalFolder = path.resolve(saveFolder, "originals");
33 | fs.ensureDir(originalFolder, () => {
34 | });
35 | tmpFolder = settings.getSetting('tmpFolder');
36 |
37 | if (!initialized) {
38 | initialized = true;
39 | ipcMain.on('wmtsGen_generate', async (event, mapID, width, height, tinSerial, extKey, hash) => {
40 | this.generate(event, mapID, width, height, tinSerial, extKey, hash);
41 | });
42 | }
43 | },
44 | async generate(ev, mapID, width, height, tinSerial, extKey, hash) {
45 | try {
46 | const self = this;
47 | const tin = new Tin({});
48 | tin.setCompiled(tinSerial);
49 | extKey = extKey ? extKey : 'jpg';
50 | const imagePath = path.resolve(originalFolder, `${mapID}.${extKey}`);
51 | const tileRoot = path.resolve(wmtsFolder, mapID);
52 |
53 | const lt = tin.transform([0, 0], false, true);
54 | const rt = tin.transform([width, 0], false, true);
55 | const rb = tin.transform([width, height], false, true);
56 | const lb = tin.transform([0, height], false, true);
57 |
58 | const pixelLongest = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
59 | const ltrbLong = Math.sqrt(Math.pow(lt[0] - rb[0], 2) + Math.pow(lt[1] - rb[1], 2));
60 | const rtlbLong = Math.sqrt(Math.pow(rt[0] - lb[0], 2) + Math.pow(rt[1] - lb[1], 2));
61 |
62 | const wwRate = MERC_MAX * 2 / 256;
63 | const mapRate = Math.min(ltrbLong / pixelLongest, rtlbLong / pixelLongest);
64 | const maxZoom = Math.ceil(Math.log2(wwRate / mapRate));
65 | const minSide = Math.min(width, height);
66 | const deltaZoom = Math.ceil(Math.log2(minSide / 256));
67 | const minZoom = maxZoom - deltaZoom;
68 |
69 | const edgeValues = [lt, lb, rt, rb];
70 | for (let px = 1; px < width; px++) {
71 | edgeValues.push(tin.transform([px, 0], false, true));
72 | edgeValues.push(tin.transform([px, height], false, true));
73 | }
74 | for (let py = 1; py < height; py++) {
75 | edgeValues.push(tin.transform([0, py], false, true));
76 | edgeValues.push(tin.transform([width, py], false, true));
77 | }
78 | const txs = edgeValues.map((item) => item[0]);
79 | const tys = edgeValues.map((item) => item[1]);
80 |
81 | const pixelXw = (Math.min(...txs) + MERC_MAX) / (2 * MERC_MAX) * 256 * Math.pow(2, maxZoom);
82 | const pixelXe = (Math.max(...txs) + MERC_MAX) / (2 * MERC_MAX) * 256 * Math.pow(2, maxZoom);
83 | const pixelYn = (MERC_MAX - Math.max(...tys)) / (2 * MERC_MAX) * 256 * Math.pow(2, maxZoom);
84 | const pixelYs = (MERC_MAX - Math.min(...tys)) / (2 * MERC_MAX) * 256 * Math.pow(2, maxZoom);
85 |
86 | const tileXw = Math.floor(pixelXw / 256);
87 | const tileXe = Math.floor(pixelXe / 256);
88 | const tileYn = Math.floor(pixelYn / 256);
89 | const tileYs = Math.floor(pixelYs / 256);
90 |
91 | const processArray = [];
92 | for (let z = maxZoom; z >= minZoom; z--) {
93 | const txw = Math.floor(tileXw / Math.pow(2, maxZoom - z));
94 | const txe = Math.floor(tileXe / Math.pow(2, maxZoom - z));
95 | const tyn = Math.floor(tileYn / Math.pow(2, maxZoom - z));
96 | const tys = Math.floor(tileYs / Math.pow(2, maxZoom - z));
97 | for (let x = txw; x <= txe; x++) {
98 | for (let y = tyn; y <= tys; y++) {
99 | processArray.push([z, x, y]);
100 | }
101 | }
102 | }
103 |
104 | const imageJimp = await Jimp.read(imagePath);
105 | const imageBuffer = imageJimp.bitmap.data;
106 |
107 | const progress = new ProgressReporter("mapedit", processArray.length, 'wmtsgenerate.generating_tile');
108 | progress.update(ev, 0);
109 |
110 | for (let i = 0; i < processArray.length; i++) {
111 | const process = processArray[i];
112 | if (process[0] === maxZoom) {
113 | await self.maxZoomTileLoop(tin, process[0], process[1], process[2], imageBuffer, width, height, tileRoot);
114 | } else {
115 | await self.upperZoomTileLoop(process[0], process[1], process[2], tileRoot);
116 | }
117 | await new Promise((s) => setTimeout(s, 1)); // eslint-disable-line no-undef
118 | progress.update(ev, i + 1);
119 | }
120 | ev.reply('wmtsGen_wmtsGenerated', {
121 | hash
122 | });
123 | } catch (err) {
124 | console.log(err); // eslint-disable-line no-undef
125 | ev.reply('wmtsGen_wmtsGenerated', {
126 | err,
127 | hash
128 | });
129 | }
130 | },
131 | async upperZoomTileLoop(z, x, y, tileRoot) {
132 | const downZoom = z + 1;
133 |
134 | const tileJimp = await new Jimp(256, 256);
135 |
136 | for (let dx = 0; dx < 2; dx++) {
137 | const ux = x * 2 + dx;
138 | const ox = dx * 128;
139 | for (let dy = 0; dy < 2; dy ++) {
140 | const uy = y * 2 + dy;
141 | const oy = dy * 128;
142 | const upImage = path.resolve(tileRoot, `${downZoom}`, `${ux}`, `${uy}.png`);
143 | try {
144 | const imageJimp = (await Jimp.read(upImage)).resize(128, 128);
145 | await tileJimp.composite(imageJimp, ox, oy);
146 | } catch(e) { // eslint-disable-line no-empty
147 | }
148 | }
149 | }
150 |
151 | const tileFolder = path.resolve(tileRoot, `${z}`, `${x}`);
152 | const tileFile = path.resolve(tileFolder, `${y}.png`);
153 | await fs.ensureDir(tileFolder);
154 | await tileJimp.write(tileFile);
155 | },
156 | async maxZoomTileLoop(tin, z, x, y, imageBuffer, width, height, tileRoot) {
157 | const self = this;
158 | const unitPerPixel = (2 * MERC_MAX) / (256 * Math.pow(2, z));
159 | const startPixelX = x * 256;
160 | const startPixelY = y * 256;
161 |
162 | const tileJimp = await new Jimp(256, 256);
163 | const tileData = tileJimp.bitmap.data;
164 |
165 | const range = [-1, 0, 1, 2];
166 | let pos = 0;
167 |
168 | for (let py = 0; py < 256; py++) {
169 | const my = MERC_MAX - ((py + startPixelY) * unitPerPixel);
170 | for (let px = 0; px < 256; px++) {
171 | const mx = (px + startPixelX) * unitPerPixel - MERC_MAX;
172 | const xy = tin.transform([mx, my], true, true);
173 | const rangeX = range.map((i) => i + ~~xy[0]);
174 | const rangeY = range.map((i) => i + ~~xy[1]);
175 |
176 | let r = 0, g = 0, b = 0, a = 0;
177 | for (const y of rangeY) {
178 | const weightY = self.getWeight(y, xy[1]);
179 | for (const x of rangeX) {
180 | const weight = weightY * self.getWeight(x, xy[0]);
181 | if (weight === 0) {
182 | continue;
183 | }
184 |
185 | const color = self.rgba(imageBuffer, width, height, x, y);
186 | r += color.r * weight;
187 | g += color.g * weight;
188 | b += color.b * weight;
189 | a += color.a * weight;
190 | }
191 | }
192 |
193 | tileData[pos] = ~~r;
194 | tileData[pos+1] = ~~g;
195 | tileData[pos+2] = ~~b;
196 | tileData[pos+3] = ~~a;
197 |
198 | pos = pos + 4;
199 | }
200 | }
201 |
202 | tileJimp.bitmap.data = tileData;
203 |
204 | const tileFolder = path.resolve(tileRoot, `${z}`, `${x}`);
205 | const tileFile = path.resolve(tileFolder, `${y}.png`);
206 | await fs.ensureDir(tileFolder);
207 | await tileJimp.write(tileFile);
208 | },
209 | norm(val) {
210 | const ret = ~~val;
211 | return ret < 0 ? 0 : ret;
212 | },
213 | rgba(pixels, w, h, x, y) {
214 | if (x < 0 || y < 0 || x >= w || y >= h) {
215 | return {r: 0, g: 0, b: 0, a: 0};
216 | }
217 | const p = ((w * y) + x) * 4;
218 | return { r: pixels[p], g: pixels[p+1], b: pixels[p+2], a: pixels[p+3]};
219 | },
220 | getWeight(t1, t2) {
221 | const a = -1;
222 | const d = Math.abs(t1 - t2);
223 | if (d < 1) {
224 | return (a + 2) * Math.pow(d, 3) - (a + 3) * Math.pow(d, 2) + 1;
225 | } else if (d < 2) {
226 | return a * Math.pow(d, 3) - 5 * a * Math.pow(d, 2) + 8 * a * d - 4 * a;
227 | } else {
228 | return 0;
229 | }
230 | }
231 | };
232 |
233 | module.exports = WmtsGenerator; // eslint-disable-line no-undef
--------------------------------------------------------------------------------
/locales/ja/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "reset": "リセット",
4 | "save": "保存",
5 | "save_with_english": "保存(Save)",
6 | "download": "ダウンロード",
7 | "language": "言語",
8 | "japanese": "日本語",
9 | "english": "英語",
10 | "germany": "ドイツ語",
11 | "french": "フランス語",
12 | "spanish": "スペイン語",
13 | "korean": "韓国語",
14 | "simplified": "中国語簡体字",
15 | "traditional": "中国語繁体字"
16 | },
17 | "navbar": {
18 | "edit_map": "地図編集",
19 | "edit_app": "アプリ編集",
20 | "settings": "設定(Settings)"
21 | },
22 | "settings": {
23 | "basic_settings": "基本設定",
24 | "base_map": "ベースマップ設定",
25 | "original_map": "オリジナル地図設定",
26 | "switch_lang": "言語切り替え(Switch Language)",
27 | "data_folder": "データフォルダ",
28 | "specify_data_folder": "データフォルダを指定してください。",
29 | "confirm_close_no_save": "設定に変更が加えられていますが保存されていません。\n保存せずに閉じてよいですか?"
30 | },
31 | "maplist": {
32 | "new_create": "新規作成",
33 | "delete_error": "削除が失敗しました。",
34 | "delete_confirm": "{{name}}を削除しますか?\n(この処理は元に戻せません)",
35 | "delete_menu": "削除メニュー",
36 | "delete_item": "{{name}}を削除",
37 | "migrating": "データベース移行中...",
38 | "migrated": "データベース移行完了",
39 | "deleting_old": "旧データファイル削除中...",
40 | "deleted_old": "旧データファイル削除完了",
41 | "migration_confirm": "旧仕様の地図データファイルがあります。\nデータベースへの移行を行いますか?",
42 | "delete_old_confirm": "新仕様へのデータ移行が完了しました。\n旧仕様のデータファイルを削除しますか?",
43 | "search_placeholder": "検索条件を入力してください"
44 | },
45 | "dataio": {
46 | "import_map_data": "地図データ入力",
47 | "import_title": "データセット入力",
48 | "export_title": "データセット出力",
49 | "import_csv": "CSV入力",
50 | "csv_file": "CSVファイル",
51 | "import_csv_status": "入力設定エラー状態",
52 | "import_csv_submit": "CSVファイル入力",
53 | "csv_error_column_dup": "カラムが重複しています",
54 | "csv_error_column_null": "カラムの値が異常です",
55 | "csv_error_ignore_header": "無視ヘッダ行数が異常です",
56 | "csv_error_proj_text": "PROJテキストが解釈できません",
57 | "column": "カラム",
58 | "pix_x_column": "ピクセルX",
59 | "pix_y_column": "ピクセルY",
60 | "lng_column": "地理座標系X",
61 | "lat_column": "地理座標系Y",
62 | "settings_title": "各種設定",
63 | "proj_text": "PROJテキスト",
64 | "revert_pix_y": "ピクセルYを負の値にする",
65 | "ignore_headers": "無視する先頭行",
66 | "use_geo_referencer": "QGISジオリファレンサのデータを使う",
67 | "proj_text_preset": "PROJテキストプリセット",
68 | "wgs84_coord": "WGS84経緯度",
69 | "sp_merc_coord": "球面メルカトル",
70 | "other_coord": "直接入力",
71 | "error_occurs": "エラーが発生しました",
72 | "csv_format_error": "CSVファイルのフォーマットが異常です",
73 | "csv_override_confirm": "GCPは既に登録済みです。CSVを読み込むとGCPはリセットされますが、よろしいですか?"
74 | },
75 | "mapedit": {
76 | "line_selected_already": "その対応線は指定済です。",
77 | "edit_metadata": "メタデータ編集",
78 | "edit_gcp": "対応点編集",
79 | "dataset_inout": "データセット入出力",
80 | "configure_map": "地図設定",
81 | "set_default": "デフォルトに設定",
82 | "mapid": "地図ID",
83 | "input_mapid": "地図IDを入力してください。",
84 | "unique_mapid": "一意な地図IDを入力してください。",
85 | "error_set_mapid": "地図IDを指定してください。",
86 | "error_mapid_character": "地図IDは英数字とアンダーバー、ハイフンのみが使えます。",
87 | "check_uniqueness": "地図IDの一意性チェックを行ってください。",
88 | "change_mapid": "地図ID変更",
89 | "uniqueness_button": "一意性確認",
90 | "confirm_change_mapid": "地図IDを変更してよろしいですか?",
91 | "image_width": "地図画像幅",
92 | "image_height": "地図画像高さ",
93 | "extension": "拡張子",
94 | "upload_map": "地図画像登録",
95 | "map_name_repr": "地図名称 (表示用)",
96 | "map_name_repr_pf": "地図名称(表示用)を入力してください",
97 | "map_name_repr_desc": "地図の表示用名称を15文字(半角30文字)以内で入力してください。",
98 | "map_name_ofc": "地図名称(正式名)",
99 | "map_name_ofc_pf": "地図名称(正式名)を入力してください",
100 | "map_name_ofc_desc": "地図の正式名称を入力してください。",
101 | "map_author": "制作者",
102 | "map_author_pf": "地図の制作者を入力してください",
103 | "map_author_desc": "地図の制作者を入力してください。",
104 | "map_create_at": "作成時期",
105 | "map_create_at_pf": "作成時期を入力してください",
106 | "map_create_at_desc": "地図の作成時期を時代名、和暦、西暦等で入力してください。",
107 | "map_era": "対象時期",
108 | "map_era_pf": "対象時期を入力してください",
109 | "map_era_desc": "地図に表されている時期を入力してください。省略時は作成時期で代替されます。",
110 | "map_owner": "所蔵者等",
111 | "map_owner_pf": "所蔵者等を入力してください",
112 | "map_owner_desc": "地図の所蔵者、提供者等を入力してください。",
113 | "map_mapper": "マッパー",
114 | "map_mapper_pf": "マッピング実施者等を入力してください",
115 | "map_mapper_desc": "マッピング実施者等を入力してください。",
116 | "map_image_license": "地図画像ライセンス",
117 | "map_image_license_desc": "地図のライセンスを選択してください。",
118 | "cc_allright_reserved": "著作権保持",
119 | "cc_by": "クリエイティブ・コモンズ 表示",
120 | "cc_by_sa": "クリエイティブ・コモンズ 表示-継承",
121 | "cc_by_nd": "クリエイティブ・コモンズ 表示-改変禁止",
122 | "cc_by_nc": "クリエイティブ・コモンズ 表示-非営利",
123 | "cc_by_nc_sa": "クリエイティブ・コモンズ 表示-非営利-継承",
124 | "cc_by_nc_nd": "クリエイティブ・コモンズ 表示-非営利-改変禁止",
125 | "cc0": "クリエイティブ・コモンズ・ゼロ",
126 | "cc_pd": "パブリックドメイン",
127 | "map_gcp_license": "マッピングデータライセンス",
128 | "map_gcp_license_desc": "マッピングデータのライセンスを選択してください。",
129 | "map_copyright": "地図画像コピーライト",
130 | "map_copyright_pf": "地図画像のコピーライト表記を入力してください",
131 | "map_copyright_desc": "地図画像のコピーライト表記を入力してください。",
132 | "map_gcp_copyright": "マッピングデータコピーライト",
133 | "map_gcp_copyright_pf": "マッピングのコピーライト表記を入力してください",
134 | "map_gcp_copyright_desc": "マッピングのコピーライト表記を入力してください。",
135 | "map_source": "典拠",
136 | "map_source_pf": "典拠を入力してください",
137 | "map_source_desc": "地図の典拠を入力してください。",
138 | "map_tile": "地図タイルURL",
139 | "map_tile_pf": "地図タイルのURLを入力してください",
140 | "map_tile_desc": "外部の地図タイルを用いる場合、http://...{z}/{x}/{y}.{png|jpg}の形で指定してください。標準の配置位置を使う場合は設定不要です。",
141 | "map_description": "説明",
142 | "map_description_pf": "地図の説明文を入力してください",
143 | "map_description_desc": "地図の説明文を入力してください。",
144 | "map_mainlayer": "メインレイヤ",
145 | "map_sublayer": "サブレイヤ",
146 | "map_addlayer": "追加",
147 | "map_removelayer": "削除",
148 | "map_importance": "重要度",
149 | "map_importance_up": "重要度UP",
150 | "map_importance_down": "重要度DOWN",
151 | "map_priority": "前面度",
152 | "map_priority_up": "前面度UP",
153 | "map_priority_down": "前面度DOWN",
154 | "map_layer_select": "編集レイヤ選択",
155 | "map_function_select": "副機能選択",
156 | "map_outline": "外郭判定モード",
157 | "map_outline_plain": "平面図",
158 | "map_outline_birdeye": "鳥観図",
159 | "map_error": "エラーモード",
160 | "map_error_valid": "厳格",
161 | "map_error_auto": "自動",
162 | "map_error_status": "エラー状態",
163 | "map_error_too_short": "対応点が少なすぎます。",
164 | "map_error_linear": "対応点が直線的に並びすぎています。もっと散らしてください。",
165 | "map_error_outside": "対応点が地図領域の範囲外にあります。地図領域内に対応点を打ってください。",
166 | "map_error_crossing": "対応線にエラーがあります(対応線が交差しているなど)。修正してください。",
167 | "map_no_error": "エラーなし",
168 | "map_error_number": "エラー{{num}}件",
169 | "map_loose_by_error": "エラーのため簡易モード",
170 | "map_error_next": "次のエラー表示",
171 | "context_add_marker": "マーカー追加",
172 | "context_remove_marker": "マーカー削除",
173 | "context_correspond_marker": "対応マーカー表示",
174 | "context_cancel_add_marker":"マーカー追加キャンセル",
175 | "context_correspond_line_start": "対応線開始マーカー指定",
176 | "context_correspond_line_end": "対応線終了マーカー指定",
177 | "context_correspond_line_cancel": "対応線指定キャンセル",
178 | "context_correspond_line_remove": "対応線削除",
179 | "context_marker_on_line": "対応線上にマーカー追加",
180 | "context_home_remove": "ホーム位置削除",
181 | "context_home_show": "ホーム位置表示",
182 | "control_basemap": "ベースマップ",
183 | "control_put_address": "住所を指定してください",
184 | "alert_mapid_checked": "一意な地図IDです。",
185 | "alert_mapid_duplicated": "この地図IDは存在します。他のIDにしてください。",
186 | "confirm_override_image": "地図画像は既に登録されています。\n置き換えてよいですか?",
187 | "error_image_upload": "地図画像登録でエラーが発生しました。",
188 | "success_image_upload": "正常に地図画像が登録できました。",
189 | "image_uploading": "地図画像登録中です。",
190 | "export_map_data": "地図データ出力",
191 | "message_export": "出力用地図データを準備しています。",
192 | "export_success": "出力成功しました。",
193 | "imexport_canceled": "キャンセルされました。",
194 | "export_error": "処理中にエラーが発生しました。",
195 | "confirm_save": "変更を保存します。\nよろしいですか?",
196 | "copy_or_move": "地図IDが変更されています。コピーを行いますか?\nコピーの場合はOK、移動の場合はキャンセルを選んでください。",
197 | "success_save": "正常に保存できました。",
198 | "error_duplicate_id": "地図IDが重複しています。\n地図IDを変更してください。",
199 | "error_saving": "保存時エラーが発生しました。",
200 | "confirm_layer_delete": "本当にこのサブレイヤを削除してよろしいですか?",
201 | "confirm_no_save": "地図に変更が加えられていますが保存されていません。\n保存せずに閉じてよいですか?",
202 | "testerror_too_short": "変換テストに必要な対応点の数が少なすぎます。",
203 | "testerror_too_linear": "対応点が直線的に並びすぎているため、変換テストが実行できません。",
204 | "testerror_outside": "対応点が地図領域外にあるため、変換テストが実行できません。",
205 | "testerror_line": "対応線にエラーがあるため、変換テストが実行できません。",
206 | "testerror_unknown": "原因不明のエラーのため、変換テストが実行できません。",
207 | "testerror_valid_error": "厳格モードでエラーがある際は、逆変換ができません。",
208 | "testerror_outside_map": "地図領域範囲外のため、変換ができません。",
209 | "marker_id": "マーカーID",
210 | "latitude": "緯度",
211 | "longitude": "経度",
212 | "edit_layer": "地図レイヤ編集",
213 | "edit_coordinate": "座標編集"
214 | },
215 | "mapupload": {
216 | "map_image": "地図画像",
217 | "dividing_tile": "地図画像をタイル分割中",
218 | "next_thumbnail": "地図サムネイル生成中"
219 | },
220 | "mapdownload": {
221 | "adding_zip": "ダウンロードするデータを準備中",
222 | "creating_zip": "ZIPファイルを作成中"
223 | },
224 | "dataupload": {
225 | "data_zip": "地図データ"
226 | },
227 | "applist": {
228 | "not_implement": "未実装"
229 | },
230 | "mapmodel": {
231 | "untitled": "タイトル未設定",
232 | "no_title": "表示用タイトルを指定してください。",
233 | "over_title": "表示用タイトル({{lang}})を15文字(半角30文字)以内にしてください。",
234 | "image_copyright": "地図画像のコピーライト表記を指定してください。"
235 | },
236 | "wmtsgenerate": {
237 | "generate": "WMTSタイル生成",
238 | "error_generation": "WMTSタイル生成でエラーが発生しました。",
239 | "success_generation": "WMTSタイル生成に成功しました。",
240 | "generating_tile": "WMTSタイル生成中です。",
241 | "result_folder": "生成結果は\"{{folder}}\"フォルダの下に出力されます。"
242 | },
243 | "menu": {
244 | "quit": "MaplatEditorを終了",
245 | "about": "MaplatEditorについて",
246 | "edit": "編集",
247 | "undo": "元に戻す",
248 | "redo": "やり直す",
249 | "dev": "開発",
250 | "reload": "再読み込み",
251 | "tools": "開発ツールの表示",
252 | "cut": "切り取り",
253 | "copy": "コピー",
254 | "paste": "貼り付け",
255 | "select_all": "すべて選択"
256 | }
257 | }
--------------------------------------------------------------------------------
/backend/src/maplist.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const path = require('path'); // eslint-disable-line no-undef
3 | const settings = require('./settings').init(); // eslint-disable-line no-undef
4 | const fs = require('fs-extra'); // eslint-disable-line no-undef
5 | const fileUrl = require('file-url'); // eslint-disable-line no-undef
6 | const {ipcMain} = require('electron'); // eslint-disable-line no-undef
7 | const thumbExtractor = require('../lib/ui_thumbnail'); // eslint-disable-line no-undef
8 | const ProgressReporter = require('../lib/progress_reporter'); // eslint-disable-line no-undef
9 | const nedbAccessor = require('../lib/nedb_accessor'); // eslint-disable-line no-undef
10 | const storeHandler = require('@maplat/core/es5/source/store_handler'); // eslint-disable-line no-undef
11 | const roundTo = require("round-to"); // eslint-disable-line no-undef
12 |
13 | function arrayRoundTo(array, decimal) {
14 | return array.map((item) => roundTo(item, decimal));
15 | }
16 | function pointsRoundTo(points) {
17 | return points.map((point) => arrayRoundTo(point, 2));
18 | }
19 | function pointSetsRoundTo(pointsets) {
20 | return pointsets.map((pointset) => {
21 | pointset[0] = arrayRoundTo(pointset[0], 2);
22 | pointset[1] = arrayRoundTo(pointset[1], 6);
23 | return pointset;
24 | });
25 | }
26 | function edgesRoundTo(edges) {
27 | return edges.map((edge) => {
28 | edge[0] = edge[0].map((illst) => arrayRoundTo(illst, 2));
29 | edge[1] = edge[1].map((merc) => arrayRoundTo(merc, 6));
30 | return edge;
31 | });
32 | }
33 |
34 | let tileFolder;
35 | let originalFolder;
36 | let uiThumbnailFolder;
37 | let dbFile;
38 | let nedb;
39 |
40 | // For legacy use
41 | let mapFolder;
42 | let compFolder;
43 | let initialized = false;
44 |
45 | const maplist = {
46 | init() {
47 | const saveFolder = settings.getSetting('saveFolder');
48 | tileFolder = path.resolve(saveFolder, "tiles");
49 | fs.ensureDir(tileFolder, () => {});
50 | originalFolder = path.resolve(saveFolder, "originals");
51 | fs.ensureDir(originalFolder, () => {});
52 | uiThumbnailFolder = path.resolve(saveFolder, "tmbs");
53 | fs.ensureDir(uiThumbnailFolder, () => {});
54 | // For legacy
55 | mapFolder = path.resolve(saveFolder, "maps");
56 | compFolder = path.resolve(saveFolder, "compiled");
57 |
58 | dbFile = path.resolve(saveFolder, "nedb.db");
59 | nedb = nedbAccessor.getInstance(dbFile);
60 |
61 | if (!initialized) {
62 | initialized = true;
63 | ipcMain.on('maplist_start', (event) => {
64 | this.start(event);
65 | });
66 | ipcMain.on('maplist_request', (event, ...args) => {
67 | this.request(event, args);
68 | });
69 | ipcMain.on('maplist_delete', (event, ...args) => {
70 | this.delete(event, args);
71 | });
72 | ipcMain.on('maplist_deleteOld', (event) => {
73 | this.deleteOld(event);
74 | });
75 | ipcMain.on('maplist_migration', (event) => {
76 | this.migration(event);
77 | });
78 | }
79 | },
80 | async start(ev) {
81 | try {
82 | fs.statSync(compFolder);
83 | } catch (err) {
84 | this.request(ev);
85 | return;
86 | }
87 | try {
88 | fs.statSync(`${compFolder}${path.sep}.updated`);
89 | this.request(ev);
90 | } catch (err) {
91 | ev.reply('maplist_migrationConfirm');
92 | }
93 | },
94 | async migration(ev) {
95 | const maps = fs.readdirSync(compFolder);
96 | const progress = new ProgressReporter("maplist", maps.length, 'maplist.migrating', 'maplist.migrated');
97 | progress.update(ev, 0);
98 | for (let i = 0; i < maps.length; i++) {
99 | const map = maps[i];
100 | if (map.match(/\.json$/)) {
101 | const mapID = map.replace(".json", "");
102 | const jsonLoad = fs.readJsonSync(`${compFolder}${path.sep}${map}`);
103 | let json = await storeHandler.store2HistMap(jsonLoad);
104 | json = json[0];
105 | if (json.gcps) json.gcps = pointSetsRoundTo(json.gcps);
106 | if (json.edges) json.edges = edgesRoundTo(json.edges);
107 | if (json.sub_maps) {
108 | json.sub_maps = json.sub_maps.map((sub_map) => {
109 | if (sub_map.gcps) sub_map.gcps = pointSetsRoundTo(sub_map.gcps);
110 | if (sub_map.edges) sub_map.edges = edgesRoundTo(sub_map.edges);
111 | if (sub_map.bounds) sub_map.bounds = pointsRoundTo(sub_map.bounds);
112 | return sub_map;
113 | });
114 | }
115 | const histMaps = await storeHandler.store2HistMap(json);
116 | //let histMaps = json;
117 | const store = await storeHandler.histMap2Store(histMaps[0], histMaps[1]);
118 |
119 | nedb.upsert(mapID, store);
120 | }
121 | progress.update(ev, i + 1);
122 | await new Promise((res) => {
123 | setTimeout(res, 500); // eslint-disable-line no-undef
124 | });
125 | }
126 | fs.writeFileSync(`${compFolder}${path.sep}.updated`, "done");
127 |
128 | this.request(ev);
129 | ev.reply('maplist_deleteOldConfirm');
130 | },
131 | async deleteOld(ev) {
132 | const folders = [compFolder, mapFolder];
133 | const progress = new ProgressReporter("maplist", folders.length, 'maplist.deleting_old', 'maplist.deleted_old');
134 | for (let i = 0; i < folders.length; i++) {
135 | const folder = folders[i];
136 | fs.removeSync(folder);
137 | progress.update(ev, i + 1);
138 | await new Promise((res) => {
139 | setTimeout(res, 500); // eslint-disable-line no-undef
140 | });
141 | }
142 | ev.reply('maplist_deletedOld');
143 | },
144 | async request(ev, args = []) {
145 | let condition = args[0];
146 | let page = args[1] || 1;
147 | if (!condition || condition === "") condition = null;
148 | let result;
149 | let pageUpdate = 0;
150 | while (1) { // eslint-disable-line no-constant-condition
151 | result = await nedb.search(condition, (page - 1) * 20, 20);
152 | if (result.docs.length === 0 && page > 1) {
153 | page--;
154 | pageUpdate = page;
155 | } else break;
156 | }
157 | if (pageUpdate) result.pageUpdate = pageUpdate;
158 |
159 | const thumbFiles = [];
160 | result.docs = await Promise.all(result.docs.map(async (doc) => {
161 | const res = {
162 | mapID: doc._id
163 | };
164 | if (typeof doc.title === 'object') {
165 | const lang = doc.lang || 'ja';
166 | res.title = doc.title[lang];
167 | } else res.title = doc.title;
168 | res.imageExtension = doc.imageExtension || doc.imageExtention;
169 | res.width = doc.width || (doc.compiled && doc.compiled.wh && doc.compiled.wh[0]);
170 | res.height = doc.height || (doc.compiled && doc.compiled.wh && doc.compiled.wh[1]);
171 |
172 | if (!res.width || !res.height) return res;
173 |
174 | const thumbFolder = `${tileFolder}${path.sep}${res.mapID}${path.sep}0${path.sep}0`;
175 | return new Promise((resolve, reject) => { // eslint-disable-line no-unused-vars
176 | fs.readdir(thumbFolder, (err, thumbs) => {
177 | if (err) {
178 | //reject(err);
179 | resolve(res);
180 | return;
181 | }
182 | if (!thumbs) {
183 | resolve(res);
184 | return;
185 | }
186 | let thumbFile;
187 | for (let i = 0; i < thumbs.length; i++) {
188 | const thumb = thumbs[i];
189 | if (/^0\.(?:jpg|jpeg|png)$/.test(thumb)) {
190 | thumbFile = `${thumbFolder}${path.sep}${thumb}`;
191 | res.thumbnail = fileUrl(thumbFile);
192 | const uiThumbnail = `${uiThumbnailFolder}${path.sep}${res.mapID}.jpg`;
193 | const uiThumbnail_old = `${uiThumbnailFolder}${path.sep}${res.mapID}_menu.jpg`;
194 | thumbFiles.push([thumbFile, uiThumbnail, uiThumbnail_old]);
195 | }
196 | }
197 | resolve(res);
198 | });
199 | });
200 | }));
201 |
202 | ev.reply('maplist_mapList', result);
203 |
204 | thumbFiles.forEach((thumbFile) => {
205 | thumbExtractor.make_thumbnail(thumbFile[0], thumbFile[1], thumbFile[2]).then(() => {
206 | }).catch((e) => { console.log(e); }); // eslint-disable-line no-undef
207 | });
208 | },
209 | async delete(ev, args = []) {
210 | const mapID = args[0];
211 | const condition = args[1];
212 | const page = args[2];
213 | const nedb = nedbAccessor.getInstance(dbFile);
214 | try {
215 | await nedb.delete(mapID);
216 | const tile = `${tileFolder}${path.sep}${mapID}`;
217 | const thumbnail = `${uiThumbnailFolder}${path.sep}${mapID}.jpg`;
218 | await new Promise((res_, rej_) => {
219 | try {
220 | fs.statSync(tile);
221 | fs.remove(tile, (err) => {
222 | if (err) rej_(err);
223 | res_();
224 | });
225 | } catch (err) {
226 | res_();
227 | }
228 | });
229 | await new Promise((res_, rej_) => {
230 | try {
231 | fs.statSync(thumbnail);
232 | fs.remove(thumbnail, (err) => {
233 | if (err) rej_(err);
234 | res_();
235 | });
236 | } catch (err) {
237 | res_();
238 | }
239 | });
240 | await new Promise((res_, rej_) => {
241 | try {
242 | const originals = fs.readdir(originalFolder);
243 | const files = originals.filter((file) => !!file.match(new RegExp(`^${mapID}\\.`)));
244 | files.forEach((file) => {
245 | const original = `${originalFolder}${path.sep}${file}`;
246 | let error = false;
247 | fs.remove(original, (err) => {
248 | if (err) {
249 | error = true;
250 | rej_(err);
251 | }
252 | });
253 | if (!error) res_();
254 | });
255 | } catch (err) {
256 | res_();
257 | }
258 | });
259 | this.request(ev, [condition, page]);
260 | } catch (e) {
261 | ev.reply('maplist_deleteError', e);
262 | }
263 | }
264 | };
265 |
266 | module.exports = maplist; // eslint-disable-line no-undef
267 |
--------------------------------------------------------------------------------
/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "common": {
3 | "reset": "Reset",
4 | "save": "Save",
5 | "save_with_english": "Save",
6 | "download": "Download",
7 | "language": "Language",
8 | "japanese": "Japanese",
9 | "english": "English",
10 | "germany": "Germany",
11 | "french": "French",
12 | "spanish": "Spanish",
13 | "korean": "Korean",
14 | "simplified": "Simplified Chinese",
15 | "traditional": "Traditional Chinese"
16 | },
17 | "navbar": {
18 | "edit_map": "Edit Map",
19 | "edit_app": "Edit Application",
20 | "settings": "Settings"
21 | },
22 | "settings": {
23 | "basic_settings": "Basic settings",
24 | "base_map": "Base map settings",
25 | "original_map": "Original map settings",
26 | "switch_lang": "Switch Language",
27 | "data_folder": "Data Folder",
28 | "specify_data_folder": "Please specify data folder.",
29 | "confirm_close_no_save": "The settings have been changed but not saved.\nDo you want to close without saving?"
30 | },
31 | "maplist": {
32 | "new_create": "Create new map",
33 | "delete_error": "Deletion failed.",
34 | "delete_confirm": "Do you want to delete {{name}}?\n(This process is irreversible.)",
35 | "delete_menu": "Delete menu",
36 | "delete_item": "Delete {{name}}",
37 | "migrating": "Migrating database...",
38 | "migrated": "Migration is finished",
39 | "deleting_old": "Removing old data files...",
40 | "deleted_old": "Removing old data is finished",
41 | "migration_confirm": "There are map data files with old specifications.\nDo you want to migrate them to the database?",
42 | "delete_old_confirm": "The data migration to the new specification is completed.\nDo you want to delete the old data files?",
43 | "search_placeholder": "Input search condition"
44 | },
45 | "dataio": {
46 | "import_map_data": "Import map data",
47 | "import_title": "Import datasets",
48 | "export_title": "Export datasets",
49 | "import_csv": "Import CSV",
50 | "csv_file": "CSV file",
51 | "import_csv_status": "Status of CSV import settings error",
52 | "import_csv_submit": "Import CSV file",
53 | "csv_error_column_dup": "Columns are duplicated",
54 | "csv_error_column_null": "Errors in the value of column",
55 | "csv_error_ignore_header": "Errors in the number of ignoring header",
56 | "csv_error_proj_text": "PROJ text cannot be analyzed",
57 | "column": "Column",
58 | "pix_x_column": "Pixel X",
59 | "pix_y_column": "Pixel Y",
60 | "lng_column": "Geo column X",
61 | "lat_column": "Geo column Y",
62 | "settings_title": "Several settings",
63 | "proj_text": "PROJ text",
64 | "revert_pix_y": "Invert pixel Y",
65 | "ignore_headers": "Number of ignoring headers",
66 | "use_geo_referencer": "Use format of QGIS georeferencer",
67 | "proj_text_preset": "PROJ text preset",
68 | "wgs84_coord": "WGS84 coordinates",
69 | "sp_merc_coord": "Spherical mercator",
70 | "other_coord": "Direct input",
71 | "error_occurs": "Error occurs",
72 | "csv_format_error": "CSV file format is invalid",
73 | "csv_override_confirm": "GCP was already registered.Importing the CSV will reset the GCP, is this OK?"
74 | },
75 | "mapedit": {
76 | "line_selected_already": "This corresponding line is already specified.",
77 | "edit_metadata": "Edit Metadata",
78 | "edit_gcp": "Edit GCP",
79 | "dataset_inout": "Input/output dataset",
80 | "configure_map": "Configure Map",
81 | "set_default": "Set as default",
82 | "mapid": "Map ID",
83 | "input_mapid": "Enter Map ID.",
84 | "unique_mapid": "Enter unique Map ID.",
85 | "error_set_mapid": "Specify Map ID.",
86 | "error_mapid_character": "Only alphanumeric characters, under-bar & hyphen can be used for Map ID.",
87 | "check_uniqueness": "Check uniqueness of Map ID.",
88 | "change_mapid": "Change Map ID",
89 | "uniqueness_button": "Check uniqueness",
90 | "confirm_change_mapid": "Are you sure you want to change the Map ID?",
91 | "image_width": "Image Width",
92 | "image_height": "Image Height",
93 | "extension": "Extension",
94 | "upload_map": "Register Map Image",
95 | "map_name_repr": "Map Name (For representation)",
96 | "map_name_repr_pf": "Enter map name for representation.",
97 | "map_name_repr_desc": "Enter map name for representation within 30 half-width (15 full-width) characters.",
98 | "map_name_ofc": "Map Name (Full)",
99 | "map_name_ofc_pf": "Enter full, detailed map name.",
100 | "map_name_ofc_desc": "Enter full, detailed map name.",
101 | "map_author": "Author",
102 | "map_author_pf": "Enter author name of the map.",
103 | "map_author_desc": "Enter author name of the map.",
104 | "map_create_at": "Created At",
105 | "map_create_at_pf": "Enter the time at when the map is created.",
106 | "map_create_at_desc": "Enter the time at when the map is created. Any representation is acceptable.",
107 | "map_era": "Target Era",
108 | "map_era_pf": "Enter the era for the map subject.",
109 | "map_era_desc": "Enter the era for the map subject. If not entered, it is replaced by \"Created At\".",
110 | "map_owner": "Owner",
111 | "map_owner_pf": "Enter owner of the map.",
112 | "map_owner_desc": "Enter owner or donor of the map.",
113 | "map_mapper": "Mapper",
114 | "map_mapper_pf": "Enter the person who did mapping GCPs od the map.",
115 | "map_mapper_desc": "Enter the person who did mapping GCPs od the map.",
116 | "map_image_license": "License of Map Image",
117 | "map_image_license_desc": "Enter the license of map image.",
118 | "cc_allright_reserved": "All Right Reserved",
119 | "cc_by": "Creative Commons Attribution",
120 | "cc_by_sa": "Creative Commons Attribution-ShareAlike",
121 | "cc_by_nd": "Creative Commons Attribution-NoDerivs",
122 | "cc_by_nc": "Creative Commons Attribution-NonCommercial",
123 | "cc_by_nc_sa": "Creative Commons Attribution-NonCommercial-ShareAlike",
124 | "cc_by_nc_nd": "Creative Commons Attribution-NonCommercial-NoDerivs",
125 | "cc0": "Creative Commons Zero",
126 | "cc_pd": "Public Domain",
127 | "map_gcp_license": "License of Mapping Data",
128 | "map_gcp_license_desc": "Enter the license of mapping data.",
129 | "map_copyright": "Map Image Copyright",
130 | "map_copyright_pf": "Enter the copyright description of map image.",
131 | "map_copyright_desc": "Enter the copyright description of map image.",
132 | "map_gcp_copyright": "Mapping Data Copyright",
133 | "map_gcp_copyright_pf": "Enter the copyright description of mapping data.",
134 | "map_gcp_copyright_desc": "Enter the copyright description of mapping data.",
135 | "map_source": "Source",
136 | "map_source_pf": "Enter the source of Map.",
137 | "map_source_desc": "Enter the source of Map.",
138 | "map_tile": "Map Image Tile URL",
139 | "map_tile_pf": "Enter the URL of map image tile.",
140 | "map_tile_desc": "If you use an external map tile, enter the format http://...{z}/{x}/{y}.{png|jpg}. No setting is required when using the standard placement position.",
141 | "map_description": "Description",
142 | "map_description_pf": "Enter the description of the map.",
143 | "map_description_desc": "Enter the description of the map.",
144 | "map_mainlayer": "Main Layer",
145 | "map_sublayer": "Sub Layer ",
146 | "map_addlayer": "Add",
147 | "map_removelayer": "Remove",
148 | "map_importance": "Importance",
149 | "map_importance_up": "Raise importance",
150 | "map_importance_down": "Reduce importance",
151 | "map_priority": "Priority",
152 | "map_priority_up": "Raise priority",
153 | "map_priority_down": "Reduce priority",
154 | "map_layer_select": "Select editing layer",
155 | "map_function_select": "Select sub function",
156 | "map_outline": "Outline Mode",
157 | "map_outline_plain": "Plain",
158 | "map_outline_birdeye": "Birdeye view",
159 | "map_error": "Error Mode",
160 | "map_error_valid": "Valid",
161 | "map_error_auto": "Auto",
162 | "map_error_status": "Error Status",
163 | "map_error_too_short": "Too few GCPs",
164 | "map_error_linear": "GCPs are too linear. Scatter them more.",
165 | "map_error_outside": "Some GCPs are outside the map area. Place them in the map area.",
166 | "map_error_crossing": "Error in corresponding lines (They intersect, etc.). Please correct it.",
167 | "map_no_error": "No Errors",
168 | "map_error_number": "{{num}} errors",
169 | "map_loose_by_error": "In loose mode, because errors have occurred.",
170 | "map_error_next": "Show next error",
171 | "context_add_marker": "Add Marker",
172 | "context_remove_marker": "Remove Marker",
173 | "context_correspond_marker": "Show Corresponding Marker",
174 | "context_cancel_add_marker":"Cancel Adding Marker",
175 | "context_correspond_line_start": "Corresponding Line Start From This Marker",
176 | "context_correspond_line_end": "Corresponding Line End To This Marker",
177 | "context_correspond_line_cancel": "Cancel Making Corresponding Line",
178 | "context_correspond_line_remove": "Remove Corresponding Line",
179 | "context_marker_on_line": "Add Marker On Corresponding Line",
180 | "context_home_remove": "Delete Home Position",
181 | "context_home_show": "Show Home Position",
182 | "control_basemap": "Base Map",
183 | "control_put_address": "Put address here",
184 | "alert_mapid_checked": "This is unique map ID. Confirmed.",
185 | "alert_mapid_duplicated": "Map ID is duplicated. Change to another ID.",
186 | "confirm_override_image": "Map image was already submitted.\nAre you sure you want to replace it?",
187 | "error_image_upload": "Error occurs while registering map image.",
188 | "success_image_upload": "Map image is successfully registered.",
189 | "image_uploading": "Registering map image...",
190 | "export_map_data": "Export map data",
191 | "message_export": "Now preparing map data for exporting...",
192 | "export_success": "Successfully exported.",
193 | "imexport_canceled": "Canceled.",
194 | "export_error": "Error occurred while processing.",
195 | "confirm_save": "Save your changes. \nAre you sure?",
196 | "copy_or_move": "Map ID has been changed. Do you want to make a copy?\nSelect OK to copy, Cancel to move.",
197 | "success_save": "Save succeeded.",
198 | "error_saving": "Error occurred during save.",
199 | "confirm_layer_delete": "Are you sure you want to delete this sublayer?",
200 | "confirm_no_save": "The map data has been changed but not saved.\nAre you sure you want to close without saving?",
201 | "testerror_too_short": "Too few GCPs for projection test.",
202 | "testerror_too_linear": "The projection test cannot be performed because GCPs are too linear.",
203 | "testerror_outside": "The projection test cannot be performed because some GCPs are outside the map area.",
204 | "testerror_line": "The projection test cannot be performed because there are some errors in corresponding lines.",
205 | "testerror_unknown": "The projection test cannot be performed because of unknown error.",
206 | "testerror_valid_error": "If there are any errors in strict mode, the inverse projection test is not possible.",
207 | "testerror_outside_map": "Cannot perform projection test because it is outside the map area.",
208 | "marker_id": "Marker ID",
209 | "latitude": "Latitude",
210 | "longitude": "Longitude",
211 | "edit_layer": "Edit map layer",
212 | "edit_coordinate": "Edit coordinate"
213 | },
214 | "mapupload": {
215 | "map_image": "Map Image",
216 | "dividing_tile": "Splitting map image into tiles",
217 | "next_thumbnail": "Generating map thumbnail"
218 | },
219 | "mapdownload": {
220 | "adding_zip": "Preparing data for download",
221 | "creating_zip": "Generating zip file"
222 | },
223 | "dataupload": {
224 | "data_zip": "Map Data"
225 | },
226 | "applist": {
227 | "not_implement": "Not implemented yet"
228 | },
229 | "mapmodel": {
230 | "untitled": "Untitled",
231 | "no_title": "Enter title for representation.",
232 | "over_title": "Enter {{lang}} title for representation within 30 half-width characters (or 15 full-width characters).",
233 | "image_copyright": "Enter representation of map image's copyright."
234 | },
235 | "wmtsgenerate": {
236 | "generate": "WMTS Tile Generation",
237 | "error_generation": "Error in WMTS tile generation.",
238 | "success_generation": "WMTS tile generation succeeded.",
239 | "generating_tile": "WMTS tiles are being generated.",
240 | "result_folder": "Generated results are outputted under \"{{folder}}\" folder."
241 | },
242 | "menu": {
243 | "quit": "Quit MaplatEditor",
244 | "about": "About MaplatEditor",
245 | "edit": "Edit",
246 | "undo": "Undo",
247 | "redo": "Redo",
248 | "dev": "Development",
249 | "reload": "Reload",
250 | "tools": "Toggle DevTools",
251 | "cut": "Cut",
252 | "copy": "Copy",
253 | "paste": "Paste",
254 | "select_all": "Select All"
255 | }
256 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Maplat Limited License
2 | Ver. 1.1 Mar. 9 2021
3 |
4 | 残念ながら、悪質なフリーライダーの発生により、Maplat関連のライブラリはオープンソースであることを一時放棄し、制限ライセンスで公開せざるを得なくなりました。
5 | 本ライセンスのVer.1.1を公開した2021年3月9日以降、問題が解決し再びApache 2.0ライセンスに戻せるようになるまでの間、MaplatEditor Version 0.5.0以降のバージョンを制限ライセンスで公開します。
6 | 制限期間のライセンス条項は以下の通りです。
7 |
8 | 1. 次項以降に記されない一般の利用者については、通常のApache 2.0に等しい条件での利用を認める。ただし、本ライセンスとApache 2.0ライセンスが矛盾する場合は、本ライセンスが優先される。
9 | 2. しかし、下記に記す特定のユーザについては、ライブラリの利用、コードの閲覧や再利用、また利用に伴う特許の知的財産利用権など、本ライブラリの利用に関する一切の権限を認めない。
10 | * 株式会社コギト 日本、京都市中京区錦小路通烏丸西入ル占出山町311 アニマート錦5F
11 | 3. 本ライセンスの記載内容について日本語表記と英語表記が矛盾する場合、日本語表記での内容が優先される。
12 |
13 | Unfortunately, due to the occurrence of malicious free riders, we have been forced to temporarily abandon the Maplat-related libraries as open source and release them under a restricted license.
14 | After March 9, 2021, when we released Version 1.1 of this license, we will release MaplatEditor Version 0.5.0 and later versions under a restricted license until the problem is resolved and we can revert to the Apache 2.0 license again.
15 | The license terms for the restricted period are as follows:
16 |
17 | 1. For general users not listed in the next sections, the terms of the license are the same as for regular Apache 2.0 license. However, if there is any conflict between this License and the Apache 2.0 License, this License shall prevail.
18 | 2. However, the specific users listed below are not granted any rights related to the use of the library, including but not limited to the use of the library, viewing and reuse of the code, and the right to use any patents associated with the use of the library.
19 | * COGITO Inc., 5F Animato Nishiki, 311 Uradeyamacho, Nakagyo-ku, Kyoto, Japan
20 | 3. In the event of any discrepancy between the Japanese and English versions of this license, the Japanese version shall prevail.
21 |
22 | =====
23 | Reference:
24 | Apache License
25 | Version 2.0, January 2004
26 | http://www.apache.org/licenses/
27 |
28 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
29 |
30 | 1. Definitions.
31 |
32 | "License" shall mean the terms and conditions for use, reproduction,
33 | and distribution as defined by Sections 1 through 9 of this document.
34 |
35 | "Licensor" shall mean the copyright owner or entity authorized by
36 | the copyright owner that is granting the License.
37 |
38 | "Legal Entity" shall mean the union of the acting entity and all
39 | other entities that control, are controlled by, or are under common
40 | control with that entity. For the purposes of this definition,
41 | "control" means (i) the power, direct or indirect, to cause the
42 | direction or management of such entity, whether by contract or
43 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
44 | outstanding shares, or (iii) beneficial ownership of such entity.
45 |
46 | "You" (or "Your") shall mean an individual or Legal Entity
47 | exercising permissions granted by this License.
48 |
49 | "Source" form shall mean the preferred form for making modifications,
50 | including but not limited to software source code, documentation
51 | source, and configuration files.
52 |
53 | "Object" form shall mean any form resulting from mechanical
54 | transformation or translation of a Source form, including but
55 | not limited to compiled object code, generated documentation,
56 | and conversions to other media types.
57 |
58 | "Work" shall mean the work of authorship, whether in Source or
59 | Object form, made available under the License, as indicated by a
60 | copyright notice that is included in or attached to the work
61 | (an example is provided in the Appendix below).
62 |
63 | "Derivative Works" shall mean any work, whether in Source or Object
64 | form, that is based on (or derived from) the Work and for which the
65 | editorial revisions, annotations, elaborations, or other modifications
66 | represent, as a whole, an original work of authorship. For the purposes
67 | of this License, Derivative Works shall not include works that remain
68 | separable from, or merely link (or bind by name) to the interfaces of,
69 | the Work and Derivative Works thereof.
70 |
71 | "Contribution" shall mean any work of authorship, including
72 | the original version of the Work and any modifications or additions
73 | to that Work or Derivative Works thereof, that is intentionally
74 | submitted to Licensor for inclusion in the Work by the copyright owner
75 | or by an individual or Legal Entity authorized to submit on behalf of
76 | the copyright owner. For the purposes of this definition, "submitted"
77 | means any form of electronic, verbal, or written communication sent
78 | to the Licensor or its representatives, including but not limited to
79 | communication on electronic mailing lists, source code control systems,
80 | and issue tracking systems that are managed by, or on behalf of, the
81 | Licensor for the purpose of discussing and improving the Work, but
82 | excluding communication that is conspicuously marked or otherwise
83 | designated in writing by the copyright owner as "Not a Contribution."
84 |
85 | "Contributor" shall mean Licensor and any individual or Legal Entity
86 | on behalf of whom a Contribution has been received by Licensor and
87 | subsequently incorporated within the Work.
88 |
89 | 2. Grant of Copyright License. Subject to the terms and conditions of
90 | this License, each Contributor hereby grants to You a perpetual,
91 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
92 | copyright license to reproduce, prepare Derivative Works of,
93 | publicly display, publicly perform, sublicense, and distribute the
94 | Work and such Derivative Works in Source or Object form.
95 |
96 | 3. Grant of Patent License. Subject to the terms and conditions of
97 | this License, each Contributor hereby grants to You a perpetual,
98 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
99 | (except as stated in this section) patent license to make, have made,
100 | use, offer to sell, sell, import, and otherwise transfer the Work,
101 | where such license applies only to those patent claims licensable
102 | by such Contributor that are necessarily infringed by their
103 | Contribution(s) alone or by combination of their Contribution(s)
104 | with the Work to which such Contribution(s) was submitted. If You
105 | institute patent litigation against any entity (including a
106 | cross-claim or counterclaim in a lawsuit) alleging that the Work
107 | or a Contribution incorporated within the Work constitutes direct
108 | or contributory patent infringement, then any patent licenses
109 | granted to You under this License for that Work shall terminate
110 | as of the date such litigation is filed.
111 |
112 | 4. Redistribution. You may reproduce and distribute copies of the
113 | Work or Derivative Works thereof in any medium, with or without
114 | modifications, and in Source or Object form, provided that You
115 | meet the following conditions:
116 |
117 | (a) You must give any other recipients of the Work or
118 | Derivative Works a copy of this License; and
119 |
120 | (b) You must cause any modified files to carry prominent notices
121 | stating that You changed the files; and
122 |
123 | (c) You must retain, in the Source form of any Derivative Works
124 | that You distribute, all copyright, patent, trademark, and
125 | attribution notices from the Source form of the Work,
126 | excluding those notices that do not pertain to any part of
127 | the Derivative Works; and
128 |
129 | (d) If the Work includes a "NOTICE" text file as part of its
130 | distribution, then any Derivative Works that You distribute must
131 | include a readable copy of the attribution notices contained
132 | within such NOTICE file, excluding those notices that do not
133 | pertain to any part of the Derivative Works, in at least one
134 | of the following places: within a NOTICE text file distributed
135 | as part of the Derivative Works; within the Source form or
136 | documentation, if provided along with the Derivative Works; or,
137 | within a display generated by the Derivative Works, if and
138 | wherever such third-party notices normally appear. The contents
139 | of the NOTICE file are for informational purposes only and
140 | do not modify the License. You may add Your own attribution
141 | notices within Derivative Works that You distribute, alongside
142 | or as an addendum to the NOTICE text from the Work, provided
143 | that such additional attribution notices cannot be construed
144 | as modifying the License.
145 |
146 | You may add Your own copyright statement to Your modifications and
147 | may provide additional or different license terms and conditions
148 | for use, reproduction, or distribution of Your modifications, or
149 | for any such Derivative Works as a whole, provided Your use,
150 | reproduction, and distribution of the Work otherwise complies with
151 | the conditions stated in this License.
152 |
153 | 5. Submission of Contributions. Unless You explicitly state otherwise,
154 | any Contribution intentionally submitted for inclusion in the Work
155 | by You to the Licensor shall be under the terms and conditions of
156 | this License, without any additional terms or conditions.
157 | Notwithstanding the above, nothing herein shall supersede or modify
158 | the terms of any separate license agreement you may have executed
159 | with Licensor regarding such Contributions.
160 |
161 | 6. Trademarks. This License does not grant permission to use the trade
162 | names, trademarks, service marks, or product names of the Licensor,
163 | except as required for reasonable and customary use in describing the
164 | origin of the Work and reproducing the content of the NOTICE file.
165 |
166 | 7. Disclaimer of Warranty. Unless required by applicable law or
167 | agreed to in writing, Licensor provides the Work (and each
168 | Contributor provides its Contributions) on an "AS IS" BASIS,
169 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
170 | implied, including, without limitation, any warranties or conditions
171 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
172 | PARTICULAR PURPOSE. You are solely responsible for determining the
173 | appropriateness of using or redistributing the Work and assume any
174 | risks associated with Your exercise of permissions under this License.
175 |
176 | 8. Limitation of Liability. In no event and under no legal theory,
177 | whether in tort (including negligence), contract, or otherwise,
178 | unless required by applicable law (such as deliberate and grossly
179 | negligent acts) or agreed to in writing, shall any Contributor be
180 | liable to You for damages, including any direct, indirect, special,
181 | incidental, or consequential damages of any character arising as a
182 | result of this License or out of the use or inability to use the
183 | Work (including but not limited to damages for loss of goodwill,
184 | work stoppage, computer failure or malfunction, or any and all
185 | other commercial damages or losses), even if such Contributor
186 | has been advised of the possibility of such damages.
187 |
188 | 9. Accepting Warranty or Additional Liability. While redistributing
189 | the Work or Derivative Works thereof, You may choose to offer,
190 | and charge a fee for, acceptance of support, warranty, indemnity,
191 | or other liability obligations and/or rights consistent with this
192 | License. However, in accepting such obligations, You may act only
193 | on Your own behalf and on Your sole responsibility, not on behalf
194 | of any other Contributor, and only if You agree to indemnify,
195 | defend, and hold each Contributor harmless for any liability
196 | incurred by, or claims asserted against, such Contributor by reason
197 | of your accepting any such warranty or additional liability.
198 |
199 | END OF TERMS AND CONDITIONS
200 |
201 | APPENDIX: How to apply the Apache License to your work.
202 |
203 | To apply the Apache License to your work, attach the following
204 | boilerplate notice, with the fields enclosed by brackets "{}"
205 | replaced with your own identifying information. (Don't include
206 | the brackets!) The text should be enclosed in the appropriate
207 | comment syntax for the file format. We also recommend that a
208 | file or class name and description of purpose be included on the
209 | same "printed page" as the copyright notice for easier
210 | identification within third-party archives.
211 |
212 | Copyright (c) 2015- Kohei Otsuka, Code for Nara, RekishiKokudo project
213 |
214 | Licensed under the Apache License, Version 2.0 (the "License");
215 | you may not use this file except in compliance with the License.
216 | You may obtain a copy of the License at
217 |
218 | http://www.apache.org/licenses/LICENSE-2.0
219 |
220 | Unless required by applicable law or agreed to in writing, software
221 | distributed under the License is distributed on an "AS IS" BASIS,
222 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
223 | See the License for the specific language governing permissions and
224 | limitations under the License.
--------------------------------------------------------------------------------
/backend/src/mapedit.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const path = require('path'); // eslint-disable-line no-undef
3 | const settings = require('./settings').init(); // eslint-disable-line no-undef
4 | const fs = require('fs-extra'); // eslint-disable-line no-undef
5 | const fileUrl = require('file-url'); // eslint-disable-line no-undef
6 | const Tin = require('@maplat/tin').default; // eslint-disable-line no-undef
7 | const AdmZip = require('adm-zip'); // eslint-disable-line no-undef
8 | const rfs = require('recursive-fs'); // eslint-disable-line no-undef
9 | const ProgressReporter = require('../lib/progress_reporter'); // eslint-disable-line no-undef
10 | const nedbAccessor = require('../lib/nedb_accessor'); // eslint-disable-line no-undef
11 | const storeHandler = require('@maplat/core/es5/source/store_handler'); // eslint-disable-line no-undef
12 | const {dialog, ipcMain, app} = require("electron"); // eslint-disable-line no-undef
13 | const csv = require('csv-parser'); // eslint-disable-line no-undef
14 | const proj = require('proj4'); // eslint-disable-line no-undef
15 | const {normalizeRequestData} = require('../lib/utils'); // eslint-disable-line no-undef
16 |
17 | let tileFolder;
18 | let originalFolder;
19 | let thumbFolder;
20 | let tmpFolder;
21 | let dbFile;
22 | let nedb;
23 | let extentCheck;
24 | let extentBuffer;
25 |
26 | let initialized = false;
27 |
28 | const mapedit = {
29 | init() {
30 | const saveFolder = settings.getSetting('saveFolder');
31 | tileFolder = `${saveFolder}${path.sep}tiles`;
32 | fs.ensureDir(tileFolder, () => {});
33 | originalFolder = `${saveFolder}${path.sep}originals`;
34 | fs.ensureDir(originalFolder, () => {});
35 | thumbFolder = `${saveFolder}${path.sep}tmbs`;
36 | tmpFolder = settings.getSetting('tmpFolder');
37 | fs.ensureDir(thumbFolder, () => {});
38 |
39 | dbFile = `${saveFolder}${path.sep}nedb.db`;
40 | nedb = nedbAccessor.getInstance(dbFile);
41 |
42 | if (!initialized) {
43 | initialized = true;
44 | ipcMain.on('mapedit_request', (event, mapID) => {
45 | this.request(event, mapID);
46 | });
47 | ipcMain.on('mapedit_updateTin', (event, gcps, edges, index, bounds, strict, vertex) => {
48 | this.updateTin(event, gcps, edges, index, bounds, strict, vertex);
49 | });
50 | ipcMain.on('mapedit_checkExtentMap', (event, extent) => {
51 | this.checkExtentMap(event, extent);
52 | });
53 | ipcMain.on('mapedit_getTmsListOfMapID', async (event, mapID) => {
54 | const list = await this.getTmsListOfMapID(mapID);
55 | event.reply('mapedit_getTmsListOfMapID_finished', list);
56 | });
57 | ipcMain.on('mapedit_getWmtsFolder', async (event) => {
58 | const folder = await this.getWmtsFolder();
59 | event.reply('mapedit_getWmtsFolder_finished', folder);
60 | });
61 | ipcMain.on('mapedit_checkID', async (event, mapID) => {
62 | this.checkID(event, mapID);
63 | });
64 | ipcMain.on('mapedit_download', async (event, mapObject, tins) => {
65 | this.download(event, mapObject, tins);
66 | });
67 | ipcMain.on('mapedit_uploadCsv', async (event, csvRepl, csvUpSettings) => {
68 | this.uploadCsv(event, csvRepl, csvUpSettings);
69 | });
70 | ipcMain.on('mapedit_save', async (event, mapObject, tins) => {
71 | this.save(event, mapObject, tins);
72 | });
73 | }
74 | },
75 | async request(ev, mapID) {
76 | const json = await nedb.find(mapID);
77 |
78 | const res = await normalizeRequestData(json, `${tileFolder}${path.sep}${mapID}${path.sep}0${path.sep}0`);
79 |
80 | res[0].mapID = mapID;
81 | res[0].status = 'Update';
82 | res[0].onlyOne = true;
83 | ev.reply('mapedit_mapData', res);
84 | },
85 | async download(ev, mapObject, tins) {
86 | const mapID = mapObject.mapID;
87 |
88 | mapObject = await storeHandler.histMap2Store(mapObject, tins);
89 |
90 | const tmpFile = `${settings.getSetting('tmpFolder')}${path.sep}${mapID}.json`;
91 | fs.writeFileSync(tmpFile, JSON.stringify(mapObject));
92 |
93 | const targets = [
94 | [tmpFile, 'maps', `${mapID}.json`],
95 | [`${thumbFolder}${path.sep}${mapID}.jpg`, 'tmbs', `${mapID}.jpg`]
96 | ];
97 |
98 | const {dirs, files} = await rfs.read(`${tileFolder}${path.sep}${mapID}`); // eslint-disable-line no-unused-vars
99 | files.map((file) => {
100 | const localPath = path.resolve(file);
101 | const zipName = path.basename(localPath);
102 | const zipPath = path.dirname(localPath).match(/[/\\](tiles[/\\].+$)/)[1];
103 | targets.push([localPath, zipPath, zipName]);
104 | });
105 |
106 | const progress = new ProgressReporter("mapedit", targets.length, 'mapdownload.adding_zip', 'mapdownload.creating_zip');
107 | progress.update(ev, 0);
108 | const zip_file = `${tmpFolder}${path.sep}${mapID}.zip`;
109 | const zip = new AdmZip();
110 |
111 | for (let i = 0; i < targets.length; i++) {
112 | const target = targets[i];
113 | zip.addLocalFile(target[0], target[1], target[2]);
114 | progress.update(ev, i + 1);
115 | }
116 |
117 | zip.writeZip(zip_file, () => {
118 | const dialog = require('electron').dialog; // eslint-disable-line no-undef
119 | dialog.showSaveDialog({
120 | defaultPath: `${app.getPath('documents')}${path.sep}${mapID}.zip`,
121 | filters: [ {name: "Output file", extensions: ['zip']} ]
122 | }).then((ret) => {
123 | if(!ret.canceled) {
124 | fs.moveSync(zip_file, ret.filePath, {
125 | overwrite: true
126 | });
127 | ev.reply('mapedit_mapDownloadResult', 'Success');
128 | } else {
129 | fs.removeSync(zip_file);
130 | ev.reply('mapedit_mapDownloadResult', 'Canceled');
131 | }
132 | fs.removeSync(tmpFile);
133 | });
134 | });
135 | },
136 | async save(ev, mapObject, tins) {
137 | const status = mapObject.status;
138 | const mapID = mapObject.mapID;
139 | const url_ = mapObject.url_;
140 | const imageExtension = mapObject.imageExtension || mapObject.imageExtention || 'jpg';
141 | if (tins.length === 0) tins = ['tooLessGcps'];
142 | const compiled = await storeHandler.histMap2Store(mapObject, tins);
143 |
144 | const tmpFolder = `${settings.getSetting('tmpFolder')}${path.sep}tiles`;
145 | const tmpUrl = fileUrl(tmpFolder);
146 | const newTile = tileFolder + path.sep + mapID;
147 | const newOriginal = `${originalFolder}${path.sep}${mapID}.${imageExtension}`;
148 | const newThumbnail = `${thumbFolder}${path.sep}${mapID}.jpg`;
149 | const regex = new RegExp(`^${tmpUrl}`);
150 | const tmpCheck = url_ && url_.match(regex);
151 |
152 | Promise.all([
153 | new Promise(async (resolve, reject) => { // eslint-disable-line no-async-promise-executor
154 | if (status !== 'Update') {
155 | const existCheck = await nedb.find(mapID);
156 | if (existCheck) {
157 | reject('Exist');
158 | return;
159 | }
160 |
161 | if (status.match(/^(Change|Copy):(.+)$/)) {
162 | const isCopy = RegExp.$1 === 'Copy';
163 | const oldMapID = RegExp.$2;
164 | const oldTile = `${tileFolder}${path.sep}${oldMapID}`;
165 | const oldOriginal = `${originalFolder}${path.sep}${oldMapID}.${imageExtension}`;
166 | const oldThumbnail = `${thumbFolder}${path.sep}${oldMapID}.jpg`;
167 | try {
168 | await nedb.upsert(mapID, compiled);
169 | if (!isCopy) {
170 | await nedb.delete(oldMapID);
171 | }
172 | if (tmpCheck) {
173 | if (!isCopy) {
174 | await new Promise((res_, rej_) => {
175 | try {
176 | fs.statSync(oldTile);
177 | fs.remove(oldTile, (err) => {
178 | if (err) rej_(err);
179 | res_();
180 | });
181 | } catch(err) {
182 | res_();
183 | }
184 | });
185 | await new Promise((res_, rej_) => {
186 | try {
187 | fs.statSync(oldOriginal);
188 | fs.remove(oldOriginal, (err) => {
189 | if (err) rej_(err);
190 | res_();
191 | });
192 | } catch (err) {
193 | res_();
194 | }
195 | });
196 | await new Promise((res_, rej_) => {
197 | try {
198 | fs.statSync(oldThumbnail);
199 | fs.remove(oldThumbnail, (err) => {
200 | if (err) rej_(err);
201 | res_();
202 | });
203 | } catch (err) {
204 | res_();
205 | }
206 | });
207 | }
208 | } else {
209 | const process = isCopy ? fs.copy : fs.move;
210 | await new Promise((res_, rej_) => {
211 | try {
212 | fs.statSync(oldTile);
213 | process(oldTile, newTile, (err) => {
214 | if (err) rej_(err);
215 | res_();
216 | });
217 | } catch (err) {
218 | res_();
219 | }
220 | });
221 | await new Promise((res_, rej_) => {
222 | try {
223 | fs.statSync(oldOriginal);
224 | process(oldOriginal, newOriginal, (err) => {
225 | if (err) rej_(err);
226 | res_();
227 | });
228 | } catch (err) {
229 | res_();
230 | }
231 | });
232 | await new Promise((res_, rej_) => {
233 | try {
234 | fs.statSync(oldThumbnail);
235 | process(oldThumbnail, newThumbnail, (err) => {
236 | if (err) rej_(err);
237 | res_();
238 | });
239 | } catch (err) {
240 | res_();
241 | }
242 | });
243 | }
244 | resolve('Success');
245 | } catch(e) {
246 | reject('Error');
247 | }
248 | } else {
249 | try {
250 | await nedb.upsert(mapID, compiled);
251 | resolve('Success');
252 | } catch(e) {
253 | reject('Error');
254 | }
255 | }
256 | } else {
257 | try {
258 | await nedb.upsert(mapID, compiled);
259 | resolve('Success');
260 | } catch(e) {
261 | reject('Error');
262 | }
263 | }
264 | }),
265 | new Promise((resolve, reject) => {
266 | if (tmpCheck) {
267 | try {
268 | fs.statSync(newTile);
269 | fs.removeSync(newTile);
270 | } catch(err) { // eslint-disable-line no-empty
271 | }
272 | fs.move(tmpFolder, newTile, (err) => {
273 | if (err) reject(err);
274 | try {
275 | fs.statSync(newOriginal);
276 | fs.removeSync(newOriginal);
277 | } catch(err) { // eslint-disable-line no-empty
278 | }
279 | fs.move(`${newTile}${path.sep}original.${imageExtension}`, newOriginal, (err) => {
280 | if (err) reject(err);
281 | try {
282 | fs.statSync(newThumbnail);
283 | fs.removeSync(newThumbnail);
284 | } catch(err) { // eslint-disable-line no-empty
285 | }
286 | fs.move(`${newTile}${path.sep}thumbnail.jpg`, newThumbnail, (err) => {
287 | if (err) reject(err);
288 | resolve();
289 | });
290 | });
291 | });
292 | } else {
293 | resolve();
294 | }
295 | })
296 | ]).then(() => {
297 | ev.reply('mapedit_saveResult', 'Success');
298 | }).catch((err) => {
299 | ev.reply('mapedit_saveResult', err);
300 | });
301 | },
302 | async checkID(ev, id) {
303 | const json = await nedb.find(id);
304 | if (!json) ev.reply('mapedit_checkIDResult', true);
305 | else ev.reply('mapedit_checkIDResult', false);
306 | },
307 | uploadCsv(ev, csvRepl, csvUpSettings) {
308 | dialog.showOpenDialog({ defaultPath: app.getPath('documents'), properties: ['openFile'],
309 | filters: [ {name: csvRepl, extensions: []} ]}).then((ret) => {
310 | if (ret.canceled) {
311 | ev.reply('mapedit_uploadedCsv', {
312 | err: 'Canceled'
313 | });
314 | } else {
315 | const file = ret.filePaths[0];
316 | const results = [];
317 | const options = {
318 | strict: true,
319 | headers: false,
320 | skipLines: csvUpSettings.ignoreHeader ? 1 : 0
321 | };
322 | fs.createReadStream(file)
323 | .pipe(csv(options))
324 | .on('data', (data) => results.push(data))
325 | .on('end', () => {
326 | let error;
327 | const gcps = [];
328 | if (results.length === 0) error = "csv_format_error";
329 | results.forEach((line) => {
330 | if (error) return;
331 | try {
332 | const illstCoord = [];
333 | const rawGeoCoord = [];
334 | illstCoord[0] = parseFloat(line[csvUpSettings.pixXColumn - 1]);
335 | illstCoord[1] = parseFloat(line[csvUpSettings.pixYColumn - 1]);
336 | if (csvUpSettings.reverseMapY) illstCoord[1] = -1 * illstCoord[1];
337 | rawGeoCoord[0] = parseFloat(line[csvUpSettings.lngColumn - 1]);
338 | rawGeoCoord[1] = parseFloat(line[csvUpSettings.latColumn - 1]);
339 | const geoCoord = proj(csvUpSettings.projText, "EPSG:3857", rawGeoCoord);
340 | gcps.push([illstCoord, geoCoord]);
341 | } catch(e) {
342 | error = "csv_format_error";
343 | }
344 | });
345 | if (error) {
346 | ev.reply('mapedit_uploadedCsv', {
347 | err: error
348 | });
349 | } else {
350 | ev.reply('mapedit_uploadedCsv', {
351 | gcps
352 | });
353 | }
354 | })
355 | .on('error', (e) => {
356 | ev.reply('mapedit_uploadedCsv', {
357 | err: e
358 | });
359 | });
360 | }
361 | });
362 | },
363 | updateTin(ev, gcps, edges, index, bounds, strict, vertex) {
364 | const wh = index === 0 ? bounds : null;
365 | const bd = index !== 0 ? bounds : null;
366 | this.createTinFromGcpsAsync(gcps, edges, wh, bd, strict, vertex)
367 | .then((tin) => {
368 | ev.reply('mapedit_updatedTin', [index, tin]);
369 | }).catch((err) => {
370 | throw(err);
371 | });
372 | },
373 | createTinFromGcpsAsync(gcps, edges, wh, bounds, strict, vertex) {
374 | if (gcps.length < 3) return Promise.resolve('tooLessGcps');
375 | return new Promise((resolve, reject) => {
376 | const tin = new Tin({});
377 | if (wh) {
378 | tin.setWh(wh);
379 | } else if (bounds) {
380 | tin.setBounds(bounds);
381 | } else {
382 | reject('Both wh and bounds are missing');
383 | }
384 | tin.setStrictMode(strict);
385 | tin.setVertexMode(vertex);
386 | tin.setPoints(gcps);
387 | tin.setEdges(edges);
388 | tin.updateTinAsync()
389 | .then(() => {
390 | resolve(tin.getCompiled());
391 | }).catch((err) => {
392 | console.log(err); // eslint-disable-line no-console,no-undef
393 | if (err === 'SOME POINTS OUTSIDE') {
394 | resolve('pointsOutside');
395 | } else if (err.indexOf('TOO LINEAR') === 0) {
396 | resolve('tooLinear');
397 | } else if (err.indexOf('Vertex indices of edge') > -1 || err.indexOf('is degenerate!') > -1 ||
398 | err.indexOf('already exists or intersects with an existing edge!') > -1) {
399 | resolve('edgeError');
400 | } else {
401 | reject(err);
402 | }
403 | });
404 | });
405 | },
406 | getTmsList() {
407 | return settings.getSetting('tmsList');
408 | },
409 | async getTmsListOfMapID(mapID) {
410 | if (mapID) return settings.getTmsListOfMapID(mapID);
411 | else return this.getTmsList();
412 | },
413 | async checkExtentMap(ev, extent) {
414 | if (!extentCheck) {
415 | if (!(extentBuffer && extentBuffer.reduce((ret, item, idx) => ret && (item === extent[idx]), true))) {
416 | extentCheck = true;
417 | extentBuffer = extent;
418 | const mapList = await nedb.searchExtent(extent);
419 | console.log('mapList'); // eslint-disable-line no-undef
420 | ev.reply('mapedit_extentMapList', mapList);
421 | setTimeout(() => { // eslint-disable-line no-undef
422 | extent = extentCheck;
423 | extentCheck = undefined;
424 | if (extent !== true) {
425 | this.checkExtentMap(ev, extent);
426 | }
427 | }, 1000);
428 | }
429 | } else {
430 | extentCheck = extent;
431 | }
432 | },
433 | async getWmtsFolder() {
434 | const saveFolder = settings.getSetting('saveFolder');
435 | return path.resolve(saveFolder, './wmts');
436 | }
437 | };
438 |
439 | module.exports = mapedit; // eslint-disable-line no-undef
440 |
--------------------------------------------------------------------------------
/frontend/src/model/map.js:
--------------------------------------------------------------------------------
1 | import _ from '../../lib/underscore_extension';
2 | import Vue from 'vue';
3 | import {Language} from "./language";
4 | import crypto from 'crypto';
5 | import Tin from '@maplat/tin';
6 | import {transform} from "ol/proj";
7 | import proj from "proj4";
8 |
9 | Vue.config.debug = true;
10 | const langObj = Language.getSingleton();
11 |
12 | const defaultMap = {
13 | title: '',
14 | attr: '',
15 | dataAttr: '',
16 | strictMode: 'strict',
17 | vertexMode: 'plain',
18 | gcps: [],
19 | edges: [],
20 | sub_maps: [],
21 | status: 'New',
22 | officialTitle: '',
23 | author: '',
24 | era: '',
25 | createdAt: '',
26 | license: 'All right reserved',
27 | dataLicense: 'CC BY-SA',
28 | contributor: '',
29 | mapper: '',
30 | reference: '',
31 | description: '',
32 | url: '',
33 | width: undefined,
34 | height: undefined,
35 | url_: '',
36 | lang: 'ja',
37 | imageExtension: undefined,
38 | wmtsHash: undefined,
39 | wmtsFolder: '',
40 | homePosition: undefined,
41 | mercZoom: undefined
42 | };
43 | const langs = {
44 | 'ja': 'japanese',
45 | 'en': 'english',
46 | 'de': 'germany',
47 | 'fr': 'french',
48 | 'es': 'spanish',
49 | 'ko': 'korean',
50 | 'zh': 'simplified',
51 | 'zh-TW': 'traditional'
52 | };
53 | defaultMap.langs = langs;
54 | function zenHankakuLength(text) {
55 | let len = 0;
56 | const str = escape(text);
57 | for (let i=0; i 0 ? this.templateMaps_ : [];
160 | },
161 | set(maps) {
162 | this.templateMaps_ = maps;
163 | }
164 | };
165 | computed.imageExtensionCalc = function() {
166 | if (this.imageExtension) return this.imageExtension;
167 | if (this.width && this.height) return 'jpg';
168 | };
169 | computed.gcpsEditReady = function() {
170 | return (this.width && this.height && this.url_) || false;
171 | };
172 | computed.wmtsEditReady = function() {
173 | const tin = this.share.tinObjects[0];
174 | return (this.mainLayerHash && this.wmtsDirty && tin.strict_status === Tin.STATUS_STRICT);
175 | }
176 | computed.csvUpError = function() {
177 | const uiValue = this.csvUploadUiValue;
178 | if (uiValue.pixXColumn === uiValue.pixYColumn || uiValue.pixXColumn === uiValue.lngColumn || uiValue.pixXColumn === uiValue.latColumn ||
179 | uiValue.pixYColumn === uiValue.lngColumn || uiValue.pixYColumn === uiValue.latColumn || uiValue.lngColumn === uiValue.latColumn) {
180 | return "column_dup";
181 | } else if (!(typeof uiValue.pixXColumn == 'number' && typeof uiValue.pixYColumn == 'number' && typeof uiValue.lngColumn == 'number' && typeof uiValue.latColumn == 'number')) {
182 | return "column_null";
183 | } else if (!(typeof uiValue.ignoreHeader == 'number')) {
184 | return "ignore_header";
185 | } else {
186 | if (uiValue.projText === "") return "proj_text";
187 | try {
188 | proj(uiValue.projText, "EPSG:4326");
189 | return false;
190 | } catch(e) {
191 | return "proj_text";
192 | }
193 | }
194 | }
195 | computed.csvProjTextError = function() {
196 |
197 | }
198 | computed.csvProjPreset = {
199 | get() {
200 | const uiValue = this.csvUploadUiValue;
201 | return uiValue.projText === "EPSG:4326" ? "wgs84" : uiValue.projText === "EPSG:3857" ? "mercator" : "other";
202 | },
203 | set(newValue) {
204 | const uiValue = this.csvUploadUiValue;
205 | uiValue.projText = newValue === "wgs84" ? "EPSG:4326" : newValue === "mercator" ? "EPSG:3857" : "";
206 | }
207 | }
208 | computed.dirty = function() {
209 | return !_.isDeepEqual(this.map_, this.map);
210 | };
211 | computed.wmtsDirty = function() {
212 | return this.wmtsHash !== this.mainLayerHash;
213 | };
214 | computed.gcps = function() {
215 | if (this.currentEditingLayer === 0) {
216 | return this.map.gcps;
217 | } else if (this.map.sub_maps.length > 0) {
218 | return this.map.sub_maps[this.currentEditingLayer - 1].gcps;
219 | }
220 | };
221 | computed.edges = function() {
222 | if (this.currentEditingLayer === 0) {
223 | if (!this.map.edges) this.$set(this.map, 'edges', []);
224 | return this.map.edges;
225 | } else if (this.map.sub_maps.length > 0) {
226 | if (!this.map.sub_maps[this.currentEditingLayer - 1].edges) {
227 | this.$set(this.map.sub_maps[this.currentEditingLayer - 1], 'edges', []);
228 | }
229 | return this.map.sub_maps[this.currentEditingLayer - 1].edges;
230 | }
231 | };
232 | computed.tinObject = {
233 | get() {
234 | return this.tinObjects[this.currentEditingLayer];
235 | },
236 | set(newValue) {
237 | this.tinObjects.splice(this.currentEditingLayer, 1, newValue);
238 | }
239 | };
240 | computed.tinObjects = {
241 | get() {
242 | return this.share.tinObjects;
243 | },
244 | set(newValue) {
245 | this.share.tinObjects = newValue;
246 | }
247 | };
248 | computed.mainLayerHash = function() {
249 | const tin = this.share.tinObjects[0];
250 | if (!tin || typeof tin === 'string') return;
251 | const hashsum = crypto.createHash('sha1');
252 | hashsum.update(JSON.stringify(tin.getCompiled()));
253 | return hashsum.digest('hex');
254 | };
255 | computed.bounds = {
256 | get() {
257 | if (this.currentEditingLayer === 0) {
258 | return [this.width, this.height];
259 | }
260 | return this.map.sub_maps[this.currentEditingLayer - 1].bounds;
261 | },
262 | set(newValue) {
263 | if (this.currentEditingLayer !== 0) {
264 | this.map.sub_maps[this.currentEditingLayer - 1].bounds = newValue;
265 | }
266 | }
267 | };
268 | computed.error = function() {
269 | const err = {};
270 | if (this.mapID == null || this.mapID === '') err['mapID'] = 'mapedit.error_set_mapid';
271 | else if (this.mapID && !this.mapID.match(/^[\d\w_-]+$/)) err['mapID'] = 'mapedit.error_mapid_character';
272 | else if (!this.onlyOne) err['mapIDOnlyOne'] = 'mapedit.check_uniqueness';
273 | if (this.map.title == null || this.map.title === '') err['title'] = this.$t('mapmodel.no_title');
274 | else {
275 | if (typeof this.map.title != 'object') {
276 | if (zenHankakuLength(this.map.title) > 30) err['title'] = this.$t('mapmodel.over_title', {lang: this.$t(`common.${this.langs[this.lang]}`)});
277 | } else {
278 | const keys = Object.keys(this.langs);
279 | for (let i=0; i 30)
281 | err['title'] = this.$t('mapmodel.over_title', {lang: this.$t(`common.${this.langs[keys[i]]}`)});
282 | }
283 | }
284 | }
285 | if (this.map.attr == null || this.map.attr === '') err['attr'] = this.$t('mapmodel.image_copyright');
286 | if (this.blockingGcpsError) err['blockingGcpsError'] = 'blockingGcpsError';
287 | return Object.keys(err).length > 0 ? err : null;
288 | };
289 | computed.blockingGcpsError = function() {
290 | return this.tinObjects.reduce((prev, curr) => curr === 'tooLinear' || curr === 'pointsOutside' || prev, false);
291 | }
292 | computed.errorStatus = function() {
293 | const tinObject = this.tinObject;
294 | if (!tinObject) return;
295 | return typeof tinObject == 'string' ? this.tinObject :
296 | tinObject.strict_status ? tinObject.strict_status : undefined;
297 | };
298 | computed.errorNumber = function() {
299 | return this.errorStatus === 'strict_error' ? this.tinObject.kinks.bakw.features.length : 0;
300 | }
301 | computed.importanceSortedSubMaps = function() {
302 | const array = Object.assign([], this.sub_maps);
303 | array.push(0);
304 | return array.sort((a, b) => {
305 | const ac = a === 0 ? 0 : a.importance;
306 | const bc = b === 0 ? 0 : b.importance;
307 | return (ac < bc ? 1 : -1);
308 | });
309 | };
310 | computed.prioritySortedSubMaps = function() {
311 | const array = Object.assign([], this.sub_maps);
312 | return array.sort((a, b) => (a.priority < b.priority ? 1 : -1));
313 | };
314 | computed.canUpImportance = function() {
315 | const most = this.importanceSortedSubMaps[0];
316 | const mostImportance = most === 0 ? 0 : most.importance;
317 | return this.importance !== mostImportance;
318 | };
319 | computed.canDownImportance = function() {
320 | const least = this.importanceSortedSubMaps[this.importanceSortedSubMaps.length - 1];
321 | const leastImportance = least === 0 ? 0 : least.importance;
322 | return this.importance !== leastImportance;
323 | };
324 | computed.canUpPriority = function() {
325 | if (this.currentEditingLayer === 0) return false;
326 | const mostPriority = this.prioritySortedSubMaps[0].priority;
327 | return this.priority !== mostPriority;
328 | };
329 | computed.canDownPriority = function() {
330 | if (this.currentEditingLayer === 0) return false;
331 | const leastPriority = this.prioritySortedSubMaps[this.prioritySortedSubMaps.length - 1].priority;
332 | return this.priority !== leastPriority;
333 | };
334 | computed.importance = function() {
335 | return this.currentEditingLayer === 0 ? 0 : this.sub_maps[this.currentEditingLayer - 1].importance;
336 | }
337 | computed.priority = function() {
338 | return this.currentEditingLayer === 0 ? 0 : this.sub_maps[this.currentEditingLayer - 1].priority;
339 | }
340 | computed.editingID = {
341 | get() {
342 | if (this.newGcp) this.editingID_ = '';
343 | return this.newGcp ? this.newGcp[2] : this.editingID_;
344 | },
345 | set(newValue) {
346 | if (this.newGcp) {
347 | this.editingID_ = '';
348 | } else {
349 | this.editingID_ = newValue;
350 | }
351 | }
352 | }
353 | computed.editingX = {
354 | get() {
355 | return this.newGcp ? this.newGcp[0] ? this.newGcp[0][0] : '' : this.editingID === '' ? '' : this.gcps[this.editingID - 1][0][0];
356 | },
357 | set(newValue) {
358 | if (this.newGcp && this.newGcp[0]) {
359 | this.newGcp.splice(0, 1, [newValue, this.editingY]);
360 | this.$emit('setXY');
361 | } else if ((!this.newGcp) && this.editingID !== '') {
362 | this.gcps[this.editingID - 1].splice(0, 1, [newValue, this.editingY]);
363 | this.$emit('setXY');
364 | }
365 | }
366 | }
367 | computed.editingY = {
368 | get() {
369 | return this.newGcp ? this.newGcp[0] ? this.newGcp[0][1] : '' : this.editingID === '' ? '' : this.gcps[this.editingID - 1][0][1];
370 | },
371 | set(newValue) {
372 | if (this.newGcp && this.newGcp[0]) {
373 | this.newGcp.splice(0, 1, [this.editingX, newValue]);
374 | this.$emit('setXY');
375 | } else if ((!this.newGcp) && this.editingID !== '') {
376 | this.gcps[this.editingID - 1].splice(0, 1, [this.editingX, newValue]);
377 | this.$emit('setXY');
378 | }
379 | }
380 | }
381 | computed.editingLongLat = {
382 | get() {
383 | const merc = this.newGcp ? this.newGcp[1] ? this.newGcp[1] : '' : this.editingID === '' ? '' : this.gcps[this.editingID - 1][1];
384 | return merc === '' ? '' : transform(merc, 'EPSG:3857', 'EPSG:4326');
385 | },
386 | set(newValue) {
387 | const merc = transform(newValue, 'EPSG:4326', 'EPSG:3857');
388 | if (this.newGcp && this.newGcp[1]) {
389 | this.newGcp.splice(1, 1, merc);
390 | this.$emit('setLongLat');
391 | } else if ((!this.newGcp) && this.editingID !== '') {
392 | this.gcps[this.editingID - 1].splice(1, 1, merc);
393 | this.$emit('setLongLat');
394 | }
395 | }
396 | }
397 | computed.editingLong = {
398 | get() {
399 | return this.editingLongLat === '' ? '' : this.editingLongLat[0];
400 | },
401 | set(newValue) {
402 | this.editingLongLat = [newValue, this.editingLat];
403 | }
404 | }
405 | computed.editingLat = {
406 | get() {
407 | return this.editingLongLat === '' ? '' : this.editingLongLat[1];
408 | },
409 | set(newValue) {
410 | this.editingLongLat = [this.editingLong, newValue];
411 | }
412 | }
413 | computed.enableSetHomeIllst = function() {
414 | return (this.errorStatus === 'strict' || this.errorStatus === 'loose') && !this.homePosition;
415 | }
416 | computed.enableSetHomeMerc = function() {
417 | return !this.homePosition;
418 | }
419 |
420 | const VueMap = Vue.extend({
421 | i18n: langObj.vi18n,
422 | data() {
423 | return {
424 | share: {
425 | map: _.deepClone(defaultMap),
426 | map_: _.deepClone(defaultMap),
427 | currentLang: 'ja',
428 | onlyOne: false,
429 | vueInit: false,
430 | currentEditingLayer: 0,
431 | csvUploadUiValue: {
432 | pixXColumn: 1,
433 | pixYColumn: 2,
434 | lngColumn: 3,
435 | latColumn: 4,
436 | ignoreHeader: 0,
437 | reverseMapY: false,
438 | projText: "EPSG:4326"
439 | },
440 | csvProjPreset: "wgs84",
441 | tinObjects: []
442 | },
443 | langs,
444 | editingID_: '',
445 | newGcp: undefined,
446 | mappingUIRow: 'layer',
447 | templateMaps_: []
448 | };
449 | },
450 | methods: {
451 | _updateWholeGcps(gcps) {
452 | if (this.currentEditingLayer === 0) {
453 | this.map.gcps = gcps;
454 | this.$set(this.map, 'edges', []);
455 | } else if (this.map.sub_maps.length > 0) {
456 | this.map.sub_maps[this.currentEditingLayer - 1].gcps = gcps;
457 | this.$set(this.map.sub_maps[this.currentEditingLayer - 1], 'edges', []);
458 | }
459 | },
460 | csvQgisSetting() {
461 | this.csvUploadUiValue = Object.assign(this.csvUploadUiValue, {
462 | pixXColumn: 1,
463 | pixYColumn: 2,
464 | lngColumn: 3,
465 | latColumn: 4,
466 | ignoreHeader: 2,
467 | reverseMapY: true,
468 | });
469 | },
470 | setCurrentAsDefault() {
471 | this.map_ = _.deepClone(this.map);
472 | },
473 | setInitialMap(map) {
474 | const setMap = _.deepClone(defaultMap);
475 | Object.assign(setMap, map);
476 | this.map = setMap;
477 | this.map_ = _.deepClone(setMap);
478 | this.currentLang = this.lang;
479 | this.onlyOne = true;
480 | },
481 | localedGetBylocale(locale, key) {
482 | const lang = this.lang;
483 | const val = this.map[key];
484 | if (typeof val != 'object') {
485 | return lang === locale ? val : '';
486 | } else {
487 | return val[locale] != null ? val[locale] : '';
488 | }
489 | },
490 | localedGet(key) {
491 | return this.localedGetBylocale(this.currentLang, key);
492 | },
493 | localedSetBylocale(locale, key, value) {
494 | const lang = this.lang;
495 | let val = this.map[key];
496 | if (value == null) value = '';
497 | if (typeof val != 'object') {
498 | if (lang === locale) {
499 | val = value;
500 | } else if (value !== '') {
501 | const val_ = {};
502 | val_[lang] = val;
503 | val_[locale] = value;
504 | val = val_;
505 | }
506 | } else {
507 | if (value === '' && lang !== locale) {
508 | delete val[locale];
509 | const keys = Object.keys(val);
510 | if (keys.length === 0) {
511 | val = '';
512 | } else if (keys.length === 1 && keys[0] === lang) {
513 | val = val[lang];
514 | }
515 | } else {
516 | // val = _.deepClone(val);
517 | val[locale] = value;
518 | }
519 | }
520 | this.$set(this.map, key, val);
521 | },
522 | localedSet(key, value) {
523 | this.localedSetBylocale(this.currentLang, key, value);
524 | },
525 | addSubMap() {
526 | this.sub_maps.push({
527 | gcps:[],
528 | edges: [],
529 | priority: this.sub_maps.length+1,
530 | importance: this.sub_maps.length+1,
531 | bounds: [[0,0], [this.width, 0], [this.width, this.height], [0, this.height]]
532 | });
533 | this.tinObjects.push('');
534 | this.currentEditingLayer = this.sub_maps.length;
535 | this.normalizeImportance(this.importanceSortedSubMaps);
536 | this.normalizePriority(this.prioritySortedSubMaps);
537 | },
538 | removeSubMap() {
539 | if (this.currentEditingLayer === 0) return;
540 | const index = this.currentEditingLayer - 1;
541 | this.currentEditingLayer = 0;
542 | this.sub_maps.splice(index, 1);
543 | this.tinObjects.splice(index+1, 1);
544 | this.normalizeImportance(this.importanceSortedSubMaps);
545 | this.normalizePriority(this.prioritySortedSubMaps);
546 | },
547 | normalizeImportance(arr) {
548 | const zeroIndex = arr.indexOf(0);
549 | arr.map((item, index) => {
550 | if (index === zeroIndex) return;
551 | item.importance = zeroIndex - index;
552 | });
553 | },
554 | normalizePriority(arr) {
555 | arr.map((item, index) => {
556 | item.priority = arr.length - index;
557 | });
558 | },
559 | upImportance() {
560 | if (!this.canUpImportance) return;
561 | const arr = this.importanceSortedSubMaps;
562 | const target = this.currentEditingLayer === 0 ? 0 : this.sub_maps[this.currentEditingLayer-1];
563 | const index = arr.indexOf(target);
564 | arr.splice(index-1, 2, arr[index], arr[index-1]);
565 | this.normalizeImportance(arr);
566 | },
567 | downImportance() {
568 | if (!this.canDownImportance) return;
569 | const arr = this.importanceSortedSubMaps;
570 | const target = this.currentEditingLayer === 0 ? 0 : this.sub_maps[this.currentEditingLayer-1];
571 | const index = arr.indexOf(target);
572 | arr.splice(index, 2, arr[index+1], arr[index]);
573 | this.normalizeImportance(arr);
574 | },
575 | upPriority() {
576 | if (!this.canUpPriority) return;
577 | const arr = this.prioritySortedSubMaps;
578 | const index = arr.indexOf(this.sub_maps[this.currentEditingLayer-1]);
579 | arr.splice(index-1, 2, arr[index], arr[index-1]);
580 | this.normalizePriority(arr);
581 | },
582 | downPriority() {
583 | if (!this.canDownPriority) return;
584 | const arr = this.prioritySortedSubMaps;
585 | const index = arr.indexOf(this.sub_maps[this.currentEditingLayer-1]);
586 | arr.splice(index, 2, arr[index+1], arr[index]);
587 | this.normalizePriority(arr);
588 | }
589 | },
590 | computed
591 | });
592 |
593 | export default VueMap;
594 |
--------------------------------------------------------------------------------
/html/mapedit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Maplat Editor
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
382 |
383 |
384 |
385 |
386 |
387 |
390 |
391 |
392 |
393 |
{{progressText}}
394 |
395 |
396 |
397 |
400 |
401 |
402 |
403 |
404 |
405 |
407 |
408 |
409 |
410 |
411 |
--------------------------------------------------------------------------------