├── 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 | ![Maplat Logo](https://code4history.github.io/Maplat/page_imgs/maplat.png) 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 | ![MapList](https://raw.githubusercontent.com/code4history/MaplatEditor/master/maplist.png) 20 | 21 | ## MapEdit (地図編集) 22 | 23 | ![MapEdit](https://raw.githubusercontent.com/code4history/MaplatEditor/master/mapedit.png) 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 | 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 | 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 | 404 | 405 | 407 | 408 | 409 | 410 | 411 | --------------------------------------------------------------------------------