├── sync-demo.sh ├── docs ├── 1 │ ├── prototypes │ ├── self │ ├── users │ │ └── admin01 │ └── colors ├── admin01.png ├── services.png ├── index.html ├── style.css └── demo.html ├── server ├── .gitignore ├── sql │ ├── 1-create-schema.sql │ ├── 7-flip-image-field.sql │ ├── 6-temporary-floor.sql │ ├── 4-utf8mb4.sql │ ├── 5-add-object-field.sql │ ├── 3-update-at.sql │ ├── 8-objects_opt.sql │ └── 2-create-tables.sql ├── defaultConfig.json ├── package.json ├── static │ ├── filestorage.js │ ├── template │ │ ├── login.html │ │ ├── master.html │ │ └── index.html │ └── generate-html.js ├── lib │ ├── log.js │ ├── config.js │ ├── mock.js │ ├── sql.js │ ├── account-service.js │ ├── search-optimizer.js │ ├── schema.js │ ├── mysql.js │ └── profile-service.js ├── initialize.sh └── commands.js ├── .travis.yml ├── src ├── elm │ ├── API │ │ ├── Page.elm │ │ └── Cache.elm │ ├── Page │ │ ├── Map │ │ │ ├── ContextMenuContext.elm │ │ │ ├── Main.elm │ │ │ ├── LinkCopy.elm │ │ │ ├── GridLayer.elm │ │ │ ├── FloorUpdateInfoView.elm │ │ │ ├── PrintGuide.elm │ │ │ ├── URL.elm │ │ │ ├── PrototypePreviewView.elm │ │ │ ├── ClipboardOptionsView.elm │ │ │ ├── KeyOperation.elm │ │ │ ├── ContextMenu.elm │ │ │ ├── SearchResultItemView.elm │ │ │ ├── Msg.elm │ │ │ └── FloorsInfoView.elm │ │ ├── Master │ │ │ ├── Styles.elm │ │ │ ├── Main.elm │ │ │ ├── Model.elm │ │ │ ├── Msg.elm │ │ │ └── PrototypeForm.elm │ │ └── Login │ │ │ ├── Styles.elm │ │ │ └── Main.elm │ ├── Util │ │ ├── DictUtil.elm │ │ ├── StringUtil.elm │ │ ├── DecodeUtil.elm │ │ ├── StyleUtil.elm │ │ ├── IdGenerator.elm │ │ ├── UrlParser.elm │ │ ├── File.elm │ │ ├── ListUtil.elm │ │ ├── UUID.elm │ │ ├── UndoList.elm │ │ └── HttpUtil.elm │ ├── Model │ │ ├── Errors.elm │ │ ├── Person.elm │ │ ├── Prototype.elm │ │ ├── User.elm │ │ ├── SaveRequest.elm │ │ ├── ColorPalette.elm │ │ ├── Scale.elm │ │ ├── ProfilePopupLogic.elm │ │ ├── DateFormatter.elm │ │ ├── Mode.elm │ │ ├── FloorInfo.elm │ │ ├── ObjectsChange.elm │ │ ├── EditingFloor.elm │ │ ├── SearchResult.elm │ │ ├── FloorDiff.elm │ │ └── Prototypes.elm │ ├── Native │ │ ├── HtmlUtil.js │ │ ├── HttpUtil.js │ │ ├── Emoji.js │ │ ├── File.js │ │ └── ClipboardData.js │ ├── View │ │ ├── MessageBar.elm │ │ ├── HeaderView.elm │ │ ├── SearchInputView.elm │ │ ├── DialogView.elm │ │ ├── Common.elm │ │ ├── MessageBarForMainView.elm │ │ ├── PrototypePreviewView.elm │ │ ├── Icons.elm │ │ ├── CommonStyles.elm │ │ └── Theme.elm │ ├── Component │ │ ├── ImageLoader.elm │ │ ├── Dialog.elm │ │ └── FloorDeleter.elm │ └── CoreType.elm ├── template │ ├── login.html │ ├── master.html │ └── index.html └── style.css ├── .gitignore ├── README.md ├── package.json ├── elm-package.json ├── persistent-cache └── src │ ├── LocalStorage.elm │ ├── Native │ └── LocalStorage.js │ └── Dag.elm ├── watch.js └── Spec.md /sync-demo.sh: -------------------------------------------------------------------------------- 1 | mv index.js docs/demo.js 2 | -------------------------------------------------------------------------------- /docs/admin01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorksApplications/office-maker/HEAD/docs/admin01.png -------------------------------------------------------------------------------- /docs/services.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WorksApplications/office-maker/HEAD/docs/services.png -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules 3 | 4 | public 5 | template 6 | secret 7 | config.json 8 | -------------------------------------------------------------------------------- /server/sql/1-create-schema.sql: -------------------------------------------------------------------------------- 1 | CREATE SCHEMA map2 DEFAULT CHARACTER SET utf8; 2 | 3 | SET SQL_SAFE_UPDATES=0; 4 | -------------------------------------------------------------------------------- /server/sql/7-flip-image-field.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `map2`.`floors` 2 | ADD COLUMN `flipImage` TINYINT(1) NOT NULL AFTER `image`; 3 | -------------------------------------------------------------------------------- /docs/1/prototypes: -------------------------------------------------------------------------------- 1 | [{"id":"1","tenantId":"","name":"","width":56,"height":96,"backgroundColor":"#eda","color":"#000","fontSize":14,"shape":"rectangle"}] 2 | -------------------------------------------------------------------------------- /server/sql/6-temporary-floor.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `map2`.`floors` 2 | DROP COLUMN `public`, 3 | ADD COLUMN `temporary` TINYINT(1) NOT NULL AFTER `realHeight`; 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "4.2" 4 | before_script: 5 | - npm install -g elm 6 | - elm-package install -y 7 | script: sh build.sh 8 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 |

Office Maker

2 | 3 |

Architecture

4 | 5 | 6 |
7 | DEMO 8 |
9 | -------------------------------------------------------------------------------- /server/sql/4-utf8mb4.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE map2.objects CHARACTER SET = utf8mb4; 2 | ALTER SCHEMA map2 DEFAULT CHARACTER SET utf8mb4; 3 | -- also need to modify sever configuration 4 | -------------------------------------------------------------------------------- /server/sql/5-add-object-field.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `map2`.`objects` 2 | ADD COLUMN `bold` TINYINT(1) NOT NULL AFTER `shape`, 3 | ADD COLUMN `url` VARCHAR(1024) NOT NULL AFTER `bold`; 4 | -------------------------------------------------------------------------------- /docs/1/self: -------------------------------------------------------------------------------- 1 | {"id":"admin01","pass":"","role":"admin","personId":"admin01","tenantId":"","person":{"id":"admin01","empNo":"1234","name":"Admin01","org":"Sample Co.,Ltd","tel":null,"mail":"admin01@example.com","image":"./admin01.png"}} 2 | -------------------------------------------------------------------------------- /src/elm/API/Page.elm: -------------------------------------------------------------------------------- 1 | module API.Page exposing (..) 2 | 3 | 4 | top : String 5 | top = 6 | "." 7 | 8 | 9 | login : String 10 | login = 11 | "./login" 12 | 13 | 14 | master : String 15 | master = 16 | "./master" 17 | -------------------------------------------------------------------------------- /src/elm/Page/Map/ContextMenuContext.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.ContextMenuContext exposing (..) 2 | 3 | import CoreType exposing (..) 4 | 5 | 6 | type ContextMenuContext 7 | = ObjectContextMenu ObjectId 8 | | FloorInfoContextMenu FloorId 9 | -------------------------------------------------------------------------------- /docs/1/users/admin01: -------------------------------------------------------------------------------- 1 | {"id":"admin01","pass":"","role":"admin","personId":"admin01","tenantId":"","person":{"id":"admin01","empNo":"1234","name":"Admin01","org":"Sample Co.,Ltd","tel":null,"mail":"admin01@xxx.com","image":"images/users/admin01.png"}} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | node_modules 3 | elm.js 4 | 5 | repl-temp-* 6 | *.ods* 7 | npm-debug.log 8 | 9 | count.sh 10 | 11 | aws-config 12 | dynamodb_local 13 | 14 | people-register-service 15 | 16 | account-service 17 | profile-service 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Office Maker 2 | ==== 3 | 4 | Now we are making a new version. 5 | 6 | If you're looking for Elm code, it now lives [here](https://github.com/WorksApplications/office-maker-client). 7 | 8 | 9 | ## License 10 | 11 | [Apache License 2.0](LICENSE) 12 | -------------------------------------------------------------------------------- /src/elm/Page/Master/Styles.elm: -------------------------------------------------------------------------------- 1 | module Page.Master.Styles exposing (..) 2 | 3 | import View.CommonStyles as Styles 4 | 5 | 6 | type alias S = 7 | List ( String, String ) 8 | 9 | 10 | validationError : S 11 | validationError = 12 | [ ( "color", Styles.errorTextColor ) 13 | ] 14 | -------------------------------------------------------------------------------- /src/elm/Util/DictUtil.elm: -------------------------------------------------------------------------------- 1 | module Util.DictUtil exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | 5 | 6 | addAll : (a -> comparable) -> List a -> Dict comparable a -> Dict comparable a 7 | addAll toKey list dict = 8 | List.foldl (\a d -> Dict.update (toKey a) (always (Just a)) d) dict list 9 | -------------------------------------------------------------------------------- /server/sql/3-update-at.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE map2.objects 2 | ADD COLUMN updateAt BIGINT(20) NOT NULL AFTER floorVersion; 3 | 4 | ALTER TABLE map2.objects 5 | DROP COLUMN modifiedVersion; 6 | 7 | SELECT * FROM map2.floors ORDER BY id, version DESC; 8 | SELECT * FROM map2.objects ORDER BY id, floorId, floorVersion DESC; 9 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Roboto; 3 | } 4 | 5 | body { 6 | margin : 0; 7 | overflow-y: hidden; 8 | font-family: 'Roboto', sans-serif; 9 | } 10 | 11 | ul { 12 | list-style-type: none; 13 | padding-left: 0; 14 | margin : 0; 15 | } 16 | 17 | a { 18 | text-decoration: none; 19 | color: inherit; 20 | } 21 | -------------------------------------------------------------------------------- /src/elm/Model/Errors.elm: -------------------------------------------------------------------------------- 1 | module Model.Errors exposing (..) 2 | 3 | import Dom 4 | import Util.File as File 5 | import API.API as API exposing (..) 6 | 7 | 8 | type GlobalError 9 | = APIError API.Error 10 | | FileError File.Error 11 | | HtmlError Dom.Error 12 | | PasteError String 13 | | Success String 14 | | NoError 15 | -------------------------------------------------------------------------------- /src/elm/Model/Person.elm: -------------------------------------------------------------------------------- 1 | module Model.Person exposing (..) 2 | 3 | 4 | type alias Id = 5 | String 6 | 7 | 8 | type alias Person = 9 | { id : Id 10 | , name : String 11 | , post : String 12 | , mail : Maybe String 13 | , tel1 : Maybe String 14 | , tel2 : Maybe String 15 | , image : Maybe String 16 | } 17 | -------------------------------------------------------------------------------- /src/elm/Util/StringUtil.elm: -------------------------------------------------------------------------------- 1 | module Util.StringUtil exposing (..) 2 | 3 | 4 | split2 : String -> String -> Maybe ( String, String ) 5 | split2 separator s = 6 | case String.indices separator s of 7 | head :: tail -> 8 | Just ( String.slice 0 head s, String.dropLeft (head + 1) s ) 9 | 10 | _ -> 11 | Nothing 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "office-maker", 3 | "version": "1.0.0", 4 | "description": "Office Maker\r ----", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha test/test.js" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "minimatch": "^3.0.0", 13 | "slash": "^1.0.0", 14 | "watch": "^0.17.1" 15 | }, 16 | "dependencies": { 17 | "ejs": "^2.5.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/elm/Model/Prototype.elm: -------------------------------------------------------------------------------- 1 | module Model.Prototype exposing (..) 2 | 3 | import Model.Object exposing (Shape) 4 | 5 | 6 | type alias Id = 7 | String 8 | 9 | 10 | type alias Prototype = 11 | { id : Id 12 | , name : String 13 | , color : String 14 | , backgroundColor : String 15 | , width : Int 16 | , height : Int 17 | , fontSize : Float 18 | , shape : Shape 19 | , personId : Maybe String 20 | } 21 | -------------------------------------------------------------------------------- /server/defaultConfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "mysql": { 3 | "host": "localhost", 4 | "user": "root", 5 | "pass": "" 6 | }, 7 | "title": "Office Map", 8 | "accountServiceRoot": "http://localhost/accounts/api", 9 | "profileServiceRoot": "http://localhost/profiles/api", 10 | "secret": "path/to/pubkey.pem", 11 | "multiTenency": false, 12 | "log": { 13 | "access": "INFO", 14 | "system": "INFO", 15 | "error": "INFO" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/elm/Native/HtmlUtil.js: -------------------------------------------------------------------------------- 1 | var _user$project$Native_HtmlUtil = function(localRuntime) { 2 | var canvas = document.createElement('canvas'); 3 | var context = canvas.getContext('2d'); 4 | 5 | function measureText(fontFamily, fontSize, s) { 6 | context.font = fontSize + "px '" + fontFamily + "'"; 7 | var metrics = context.measureText(s); 8 | return metrics.width; 9 | } 10 | return { 11 | measureText: F3(measureText) 12 | }; 13 | }(); 14 | -------------------------------------------------------------------------------- /src/elm/Util/DecodeUtil.elm: -------------------------------------------------------------------------------- 1 | module Util.DecodeUtil exposing (..) 2 | 3 | import Json.Decode as D exposing (Decoder) 4 | import Json.Decode.Pipeline as Pipeline 5 | 6 | 7 | tuple2 : (a -> b -> c) -> Decoder a -> Decoder b -> Decoder c 8 | tuple2 f a b = 9 | D.map2 f (D.index 0 a) (D.index 1 b) 10 | 11 | 12 | 13 | -- for Pipeline 14 | 15 | 16 | optional_ : String -> Decoder a -> Decoder (Maybe a -> b) -> Decoder b 17 | optional_ key decoder = 18 | Pipeline.custom (D.maybe (D.field key decoder)) 19 | -------------------------------------------------------------------------------- /src/elm/View/MessageBar.elm: -------------------------------------------------------------------------------- 1 | module View.MessageBar exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import View.Styles as Styles 6 | 7 | 8 | none : Html msg 9 | none = 10 | div [ style Styles.noneBar ] [] 11 | 12 | 13 | success : String -> Html msg 14 | success msg = 15 | div [ style Styles.successBar ] [ text msg ] 16 | 17 | 18 | error : String -> Html msg 19 | error message = 20 | div [ class "message-bar-error", style Styles.errorBar ] [ text message ] 21 | -------------------------------------------------------------------------------- /src/elm/Page/Map/Main.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.Main exposing (..) 2 | 3 | import Navigation 4 | import Page.Map.Model exposing (Model) 5 | import Page.Map.Update as Update exposing (Flags) 6 | import Page.Map.View as View 7 | import Page.Map.Msg exposing (Msg) 8 | import Html.Lazy exposing (lazy) 9 | 10 | 11 | main : Program Flags Model Msg 12 | main = 13 | Navigation.programWithFlags Update.parseURL 14 | { init = Update.init 15 | , view = lazy View.view 16 | , update = Update.update 17 | , subscriptions = Update.subscriptions 18 | } 19 | -------------------------------------------------------------------------------- /src/elm/Page/Master/Main.elm: -------------------------------------------------------------------------------- 1 | port module Page.Master.Main exposing (..) 2 | 3 | import Html 4 | import Page.Master.Model exposing (Model) 5 | import Page.Master.Msg exposing (Msg) 6 | import Page.Master.Update exposing (Flags, init, update, subscriptions) 7 | import Page.Master.View exposing (view) 8 | 9 | 10 | port removeToken : {} -> Cmd msg 11 | 12 | 13 | port tokenRemoved : ({} -> msg) -> Sub msg 14 | 15 | 16 | main : Program Flags Model Msg 17 | main = 18 | Html.programWithFlags 19 | { init = init 20 | , view = view 21 | , update = update removeToken 22 | , subscriptions = subscriptions 23 | } 24 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "office-maker-server", 3 | "version": "1.0.0", 4 | "description": "Office Maker server implementation", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "mocha test/test.js" 8 | }, 9 | "author": "", 10 | "license": "index.js", 11 | "dependencies": { 12 | "body-parser": "^1.15.0", 13 | "express": "^4.13.4", 14 | "express-session": "^1.13.0", 15 | "fs-extra": "^0.28.0", 16 | "jsonwebtoken": "^7.1.9", 17 | "log4js": "^0.6.38", 18 | "minimatch": "^3.0.0", 19 | "mysql": "^2.10.2", 20 | "request": "^2.72.0", 21 | "slash": "^1.0.0", 22 | "uuid": "^2.0.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/elm/Model/User.elm: -------------------------------------------------------------------------------- 1 | module Model.User exposing (..) 2 | 3 | import Model.Person exposing (Person) 4 | 5 | 6 | type alias Id = 7 | String 8 | 9 | 10 | type User 11 | = Admin Person 12 | | General Person 13 | | Guest 14 | 15 | 16 | isAdmin : User -> Bool 17 | isAdmin user = 18 | case user of 19 | Admin _ -> 20 | True 21 | 22 | _ -> 23 | False 24 | 25 | 26 | isGuest : User -> Bool 27 | isGuest user = 28 | user == Guest 29 | 30 | 31 | admin : Person -> User 32 | admin person = 33 | Admin person 34 | 35 | 36 | general : Person -> User 37 | general person = 38 | General person 39 | 40 | 41 | guest : User 42 | guest = 43 | Guest 44 | -------------------------------------------------------------------------------- /server/static/filestorage.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs-extra'); 2 | 3 | var publicDir = __dirname + '/public'; 4 | 5 | function save(path, image) { 6 | return new Promise((resolve, reject) => { 7 | fs.writeFile(publicDir + '/' + path, image, (e) => { 8 | if (e) { 9 | reject(e); 10 | } else { 11 | resolve(); 12 | } 13 | }); 14 | }); 15 | } 16 | 17 | function empty(dir) { 18 | return new Promise((resolve, reject) => { 19 | fs.emptyDir(publicDir + '/' + dir, (e) => { 20 | if (e) { 21 | reject(e); 22 | } else { 23 | resolve(); 24 | } 25 | }); 26 | }); 27 | } 28 | 29 | module.exports = { 30 | save: save, 31 | empty: empty, 32 | publicDir: publicDir 33 | }; 34 | -------------------------------------------------------------------------------- /src/elm/Page/Map/LinkCopy.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.LinkCopy exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Events exposing (..) 6 | import View.CommonStyles as CS 7 | 8 | 9 | -- copy : Msg -> Cmd msg 10 | -- copy (Copy inputId) = 11 | -- copyLink inputId 12 | 13 | 14 | inputId : String 15 | inputId = 16 | "copy-link-input" 17 | 18 | 19 | view : String -> Html msg 20 | view url = 21 | div [ style styles ] 22 | [ input [ id inputId, style CS.input, value url ] [] 23 | , button 24 | [ style CS.button 25 | 26 | -- , onClick (toMsg (Copy inputId)) 27 | ] 28 | [ text "Copy" ] 29 | ] 30 | 31 | 32 | styles : List ( String, String ) 33 | styles = 34 | [] 35 | -------------------------------------------------------------------------------- /src/elm/Util/StyleUtil.elm: -------------------------------------------------------------------------------- 1 | module Util.StyleUtil exposing (..) 2 | 3 | 4 | type alias Style = 5 | List ( String, String ) 6 | 7 | 8 | px : number -> String 9 | px num = 10 | toString num ++ "px" 11 | 12 | 13 | em : number -> String 14 | em num = 15 | toString num ++ "em" 16 | 17 | 18 | ch : number -> String 19 | ch num = 20 | toString num ++ "ch" 21 | 22 | 23 | percent : number -> String 24 | percent num = 25 | toString num ++ "%" 26 | 27 | 28 | rgb : number -> number -> number -> String 29 | rgb r g b = 30 | "rgb(" ++ toString r ++ "," ++ toString g ++ "," ++ toString b ++ ")" 31 | 32 | 33 | rgba : number -> number -> number -> number -> String 34 | rgba r g b a = 35 | "rgba(" ++ toString r ++ "," ++ toString g ++ "," ++ toString b ++ "," ++ toString a ++ ")" 36 | -------------------------------------------------------------------------------- /src/elm/Native/HttpUtil.js: -------------------------------------------------------------------------------- 1 | var _user$project$Native_HttpUtil = function(localRuntime) { 2 | function sendFile(method, url, headers, file) { 3 | return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) { 4 | var xhr = new XMLHttpRequest(); 5 | xhr.open(method, url, true); 6 | headers.forEach(function(header) { 7 | xhr.setRequestHeader(header[0], header[1]); 8 | }); 9 | xhr.onload = function(e) { 10 | if (this.status == 200) { 11 | callback(_elm_lang$core$Native_Scheduler.succeed(_elm_lang$core$Native_Utils.Tuple0)) 12 | } else { 13 | callback(_elm_lang$core$Native_Scheduler.fail("")) //TODO 14 | } 15 | }; 16 | xhr.send(file); 17 | }); 18 | } 19 | return { 20 | sendFile: F4(sendFile) 21 | }; 22 | }(); 23 | -------------------------------------------------------------------------------- /src/elm/Util/IdGenerator.elm: -------------------------------------------------------------------------------- 1 | module Util.IdGenerator exposing (..) 2 | 3 | import Util.UUID as UUID 4 | 5 | 6 | type Seed 7 | = Seed UUID.Seed 8 | 9 | 10 | init : ( Int, Int ) -> Seed 11 | init ( i1, i2 ) = 12 | Seed (UUID.init i1 i2) 13 | 14 | 15 | new : Seed -> ( String, Seed ) 16 | new (Seed seed) = 17 | let 18 | ( newValue, newSeed ) = 19 | UUID.step seed 20 | in 21 | ( newValue, Seed newSeed ) 22 | 23 | 24 | zipWithNewIds : Seed -> List a -> ( List ( a, String ), Seed ) 25 | zipWithNewIds seed list = 26 | List.foldr 27 | (\a ( list, seed ) -> 28 | let 29 | ( newId, newSeed ) = 30 | new seed 31 | in 32 | ( ( a, newId ) :: list, newSeed ) 33 | ) 34 | ( [], seed ) 35 | list 36 | -------------------------------------------------------------------------------- /src/elm/Native/Emoji.js: -------------------------------------------------------------------------------- 1 | var _user$project$Native_Emoji = function(localRuntime) { 2 | function splitText(s) { 3 | var node = document.createElement('div'); 4 | node.textContent = s; 5 | twemoji.parse(node); 6 | var result = []; 7 | for (var i = 0; i < node.childNodes.length; i++) { 8 | var child = node.childNodes[i]; 9 | if (child.tagName === 'IMG') { 10 | result.push({ 11 | type: 'image', 12 | original: child.getAttribute('alt'), 13 | url: child.getAttribute('src') 14 | }); 15 | } else { 16 | result.push({ 17 | type: 'text', 18 | value: child.nodeValue 19 | }); 20 | } 21 | } 22 | return _elm_lang$core$Native_List.fromArray(result); 23 | } 24 | return { 25 | splitText: splitText 26 | }; 27 | }(); 28 | -------------------------------------------------------------------------------- /src/elm/Page/Master/Model.elm: -------------------------------------------------------------------------------- 1 | module Page.Master.Model exposing (..) 2 | 3 | import Debounce exposing (Debounce) 4 | import Component.Header as Header 5 | import Model.User as User exposing (User) 6 | import Model.I18n as I18n exposing (Language(..)) 7 | import Model.Prototype exposing (Prototype) 8 | import Model.ColorPalette as ColorPalette exposing (ColorPalette) 9 | import API.API as API 10 | import Page.Master.PrototypeForm exposing (PrototypeForm) 11 | 12 | 13 | type alias Model = 14 | { apiConfig : API.Config 15 | , title : String 16 | , allAdmins : List User 17 | , colorPalette : ColorPalette 18 | , prototypes : List PrototypeForm 19 | , error : Maybe String 20 | , header : Header.Model 21 | , lang : Language 22 | , saveColorDebounce : Debounce ColorPalette 23 | , savePrototypeDebounce : Debounce Prototype 24 | } 25 | -------------------------------------------------------------------------------- /src/elm/View/HeaderView.elm: -------------------------------------------------------------------------------- 1 | module View.HeaderView exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Lazy as Lazy 6 | import View.Styles as S 7 | 8 | 9 | view : Bool -> String -> Maybe String -> Html msg -> Html msg 10 | view printMode title link menu = 11 | header 12 | [ style (S.header printMode) 13 | ] 14 | [ h1 15 | [ style S.h1 ] 16 | [ case link of 17 | Just url -> 18 | Lazy.lazy2 headerLink url title 19 | 20 | Nothing -> 21 | text title 22 | ] 23 | , menu 24 | ] 25 | 26 | 27 | headerLink : String -> String -> Html msg 28 | headerLink url text_ = 29 | a 30 | [ style S.headerLink 31 | , href url 32 | ] 33 | [ text text_ ] 34 | -------------------------------------------------------------------------------- /src/elm/Page/Master/Msg.elm: -------------------------------------------------------------------------------- 1 | module Page.Master.Msg exposing (..) 2 | 3 | import Http 4 | import Debounce exposing (Debounce) 5 | import API.Cache as Cache exposing (Cache, UserState) 6 | import Component.Header as Header 7 | import Model.User as User exposing (User) 8 | import Model.Prototype exposing (Prototype) 9 | import Model.ColorPalette as ColorPalette exposing (ColorPalette) 10 | import Page.Master.PrototypeForm exposing (PrototypeForm) 11 | 12 | 13 | type Msg 14 | = NoOp 15 | | Loaded UserState User ColorPalette (List Prototype) (List User) 16 | | HeaderMsg Header.Msg 17 | | AddColor Bool 18 | | DeleteColor Bool Int 19 | | InputColor Bool Int String 20 | | UpdatePrototype Int PrototypeForm 21 | | SaveColorDebounceMsg Debounce.Msg 22 | | SavePrototypeDebounceMsg Debounce.Msg 23 | | NotAuthorized 24 | | APIError Http.Error 25 | -------------------------------------------------------------------------------- /server/lib/log.js: -------------------------------------------------------------------------------- 1 | var log4js = require('log4js'); 2 | var fs = require('fs'); 3 | var config = require('./config.js'); 4 | 5 | var options = { 6 | appenders: [{ 7 | category: "access", 8 | type: "dateFile", 9 | filename: "/tmp/access.log", 10 | pattern: "-yyyy-MM-dd", 11 | backups: 3 12 | }, { 13 | category: "system", 14 | type: "dateFile", 15 | filename: "/tmp/system.log", 16 | pattern: "-yyyy-MM-dd", 17 | backups: 3 18 | }], 19 | levels: config.log 20 | }; 21 | var debug = true; 22 | if (debug) { 23 | options.appenders.forEach(appender => { 24 | appender.type = 'console'; 25 | }); 26 | } 27 | 28 | log4js.configure(options); 29 | 30 | module.exports = { 31 | access: log4js.getLogger('access'), 32 | system: log4js.getLogger('system'), 33 | express: log4js.connectLogger(log4js.getLogger('access'), { 34 | level: log4js.levels.INFO 35 | }) 36 | }; 37 | -------------------------------------------------------------------------------- /src/template/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /server/sql/8-objects_opt.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE map2.objects_opt ( 2 | id VARCHAR(36) NOT NULL, 3 | type VARCHAR(16) NOT NULL, 4 | name VARCHAR(128) NOT NULL, 5 | x INT NOT NULL, 6 | y INT NOT NULL, 7 | width INT NOT NULL, 8 | height INT NOT NULL, 9 | backgroundColor VARCHAR(64) NOT NULL, 10 | color VARCHAR(64) NOT NULL, 11 | fontSize DECIMAL(4,1) NOT NULL, 12 | shape VARCHAR(64) NOT NULL, 13 | bold TINYINT(1) NOT NULL, 14 | url VARCHAR(1024) NOT NULL, 15 | personId VARCHAR(128), 16 | personName VARCHAR(128) NOT NULL, 17 | personEmpNo VARCHAR(32) NOT NULL, 18 | personPost VARCHAR(512) NOT NULL, 19 | personTel1 VARCHAR(32) NOT NULL, 20 | personTel2 VARCHAR(32) NOT NULL, 21 | personMail VARCHAR(128) NOT NULL, 22 | personImage VARCHAR(256) NOT NULL, 23 | floorId VARCHAR(36) NOT NULL, 24 | editing TINYINT(1) NOT NULL, 25 | updateAt BIGINT(20) NOT NULL, 26 | UNIQUE(id, floorId, editing) 27 | ); 28 | -------------------------------------------------------------------------------- /server/static/template/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /server/lib/config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | /* paths */ 5 | var configJsonPath = path.resolve(__dirname, '../config.json'); 6 | var defaultConfigJsonPath = path.resolve(__dirname, '../defaultConfig.json'); 7 | var replaceSecret = config => { 8 | var secretFilePath = path.resolve(__dirname, '..', config.secret); 9 | if (fs.existsSync(secretFilePath)) { 10 | config.secret = { 11 | token: fs.readFileSync(secretFilePath, 'utf8') 12 | }; 13 | } else { 14 | // It is okay on test. 15 | config.secret = null; 16 | } 17 | }; 18 | 19 | /* load */ 20 | var config = null; 21 | if (fs.existsSync(configJsonPath)) { 22 | config = JSON.parse(fs.readFileSync(configJsonPath, 'utf8')); 23 | } else { 24 | config = JSON.parse(fs.readFileSync(defaultConfigJsonPath, 'utf8')); 25 | } 26 | 27 | /* additional/replace */ 28 | replaceSecret(config); 29 | config.apiRoot = '/api'; 30 | 31 | module.exports = config; 32 | -------------------------------------------------------------------------------- /server/initialize.sh: -------------------------------------------------------------------------------- 1 | cd `dirname $0` 2 | 3 | user=root 4 | pass=root 5 | 6 | if ls config.json ; then 7 | echo "Already initialized. To reinitialize, delete config.json." 8 | exit 1; 9 | fi 10 | 11 | cat defaultConfig.json \ 12 | | sed -e "s/\"user\": \"root\"/\"user\": \"$user\"/"\ 13 | | sed -e "s/\"pass\": \"\"/\"pass\": \"$pass\"/" \ 14 | > config.json 15 | mysql --user=$user --password=$pass -e "drop database map2;" 16 | mysql --user=$user --password=$pass < sql/1-create-schema.sql 17 | mysql --user=$user --password=$pass < sql/2-create-tables.sql 18 | mysql --user=$user --password=$pass < sql/3-update-at.sql 19 | mysql --user=$user --password=$pass < sql/4-utf8mb4.sql 20 | mysql --user=$user --password=$pass < sql/5-add-object-field.sql 21 | mysql --user=$user --password=$pass < sql/6-temporary-floor.sql 22 | mysql --user=$user --password=$pass < sql/7-flip-image-field.sql 23 | mysql --user=$user --password=$pass < sql/8-objects_opt.sql 24 | node commands.js createInitialData 25 | -------------------------------------------------------------------------------- /src/template/master.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= title %> 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /server/static/template/master.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= title %> 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/elm/Native/File.js: -------------------------------------------------------------------------------- 1 | var _user$project$Native_File = function(localRuntime) { 2 | 3 | function readAsDataURL(file) { 4 | return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) { 5 | var reader = new FileReader(); 6 | reader.readAsDataURL(file); 7 | reader.onload = function() { 8 | var dataUrl = reader.result; 9 | callback(_elm_lang$core$Native_Scheduler.succeed(dataUrl)); 10 | }; 11 | reader.onerror = function() { 12 | callback(_elm_lang$core$Native_Scheduler.succeed("")); //TODO 13 | }; 14 | }); 15 | } 16 | 17 | function getSizeOfImage(dataUrl) { 18 | var image = new Image(); 19 | image.src = dataUrl; 20 | return _elm_lang$core$Native_Utils.Tuple2(image.width, image.height); 21 | } 22 | 23 | function length(fileList) { 24 | return fileList.length; 25 | } 26 | 27 | function getAt(i, fileList) { 28 | return fileList[i]; 29 | } 30 | return { 31 | readAsDataURL: readAsDataURL, 32 | getSizeOfImage: getSizeOfImage, 33 | length: length, 34 | getAt: F2(getAt) 35 | }; 36 | }(); 37 | -------------------------------------------------------------------------------- /src/elm/Util/UrlParser.elm: -------------------------------------------------------------------------------- 1 | module Util.UrlParser exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Util.StringUtil exposing (..) 5 | import Http 6 | 7 | 8 | type alias URL = 9 | ( List String, Dict String String ) 10 | 11 | 12 | parseSearch : String -> Dict String String 13 | parseSearch s_ = 14 | let 15 | s = 16 | String.dropLeft 1 s_ 17 | 18 | -- drop "?" 19 | list = 20 | String.split "&" s 21 | 22 | keyValue indices = 23 | case indices of 24 | head :: tail -> 25 | Just ( String.slice 0 head, String.dropLeft (head + 1) ) 26 | 27 | _ -> 28 | Nothing 29 | 30 | maybeKeyValues = 31 | List.map (split2 "=") list 32 | 33 | updateDict maybe dict = 34 | case maybe of 35 | Just ( key, value ) -> 36 | Dict.insert key (Http.decodeUri value |> Maybe.withDefault "") dict 37 | 38 | Nothing -> 39 | dict 40 | in 41 | List.foldl updateDict Dict.empty maybeKeyValues 42 | -------------------------------------------------------------------------------- /src/elm/Page/Login/Styles.elm: -------------------------------------------------------------------------------- 1 | module Page.Login.Styles exposing (..) 2 | 3 | import View.CommonStyles as Styles 4 | 5 | 6 | type alias S = 7 | List ( String, String ) 8 | 9 | 10 | loginContainer : S 11 | loginContainer = 12 | [ ( "margin-left", "auto" ) 13 | , ( "margin-right", "auto" ) 14 | , ( "margin-top", "40px" ) 15 | , ( "margin-bottom", "auto" ) 16 | , ( "width", "400px" ) 17 | , ( "padding", "15px" ) 18 | , ( "border", "solid 1px #aaa" ) 19 | ] 20 | 21 | 22 | loginCaption : S 23 | loginCaption = 24 | [] 25 | 26 | 27 | loginError : S 28 | loginError = 29 | [ ( "color", Styles.errorTextColor ) 30 | , ( "margin-bottom", "15px" ) 31 | ] 32 | 33 | 34 | loginSubmitButton : S 35 | loginSubmitButton = 36 | Styles.primaryButton ++ [ ( "margin-top", "20px" ), ( "width", "100%" ) ] 37 | 38 | 39 | formInput : S 40 | formInput = 41 | Styles.input 42 | ++ [ ( "padding", "7px 8px" ) 43 | , ( "vertical-align", "middle" ) 44 | , ( "font-size", "13px" ) 45 | , ( "margin-top", "5px" ) 46 | , ( "margin-bottom", "15px" ) 47 | ] 48 | -------------------------------------------------------------------------------- /docs/1/colors: -------------------------------------------------------------------------------- 1 | [{"id":"0","tenantId":"","ord":0,"type":"backgroundColor","color":"#eda"},{"id":"1","tenantId":"","ord":1,"type":"backgroundColor","color":"#baf"},{"id":"10","tenantId":"","ord":10,"type":"color","color":"#75a"},{"id":"11","tenantId":"","ord":11,"type":"color","color":"#c57"},{"id":"12","tenantId":"","ord":12,"type":"color","color":"#69a"},{"id":"13","tenantId":"","ord":13,"type":"color","color":"#8c5"},{"id":"14","tenantId":"","ord":14,"type":"color","color":"#5ab"},{"id":"15","tenantId":"","ord":15,"type":"color","color":"#666"},{"id":"16","tenantId":"","ord":16,"type":"color","color":"#000"},{"id":"2","tenantId":"","ord":2,"type":"backgroundColor","color":"#fba"},{"id":"3","tenantId":"","ord":3,"type":"backgroundColor","color":"#9bd"},{"id":"4","tenantId":"","ord":4,"type":"backgroundColor","color":"#af8"},{"id":"5","tenantId":"","ord":5,"type":"backgroundColor","color":"#8df"},{"id":"6","tenantId":"","ord":6,"type":"backgroundColor","color":"#bbb"},{"id":"7","tenantId":"","ord":7,"type":"backgroundColor","color":"#fff"},{"id":"8","tenantId":"","ord":8,"type":"backgroundColor","color":"rgba(255,255,255,0.5)"},{"id":"9","tenantId":"","ord":9,"type":"color","color":"#875"}] 2 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Roboto; 3 | } 4 | 5 | @page { 6 | size: A4 landscape; 7 | margin: 0; 8 | } 9 | 10 | @media print { 11 | .no-print { 12 | display: none !important; 13 | } 14 | } 15 | 16 | body { 17 | margin: 0; 18 | overflow-y: hidden; 19 | font-family: 'Roboto', sans-serif; 20 | } 21 | 22 | ul { 23 | list-style-type: none; 24 | padding-left: 0; 25 | margin: 0; 26 | } 27 | 28 | a { 29 | text-decoration: none; 30 | color: inherit; 31 | } 32 | 33 | .elm-overlay { 34 | z-index: 2147483647; 35 | } 36 | 37 | img.emoji { 38 | height: 1.4em; 39 | width: 1.4em; 40 | margin: 0 .05em 0 .1em; 41 | vertical-align: -0.1em; 42 | } 43 | 44 | 45 | /*https://gist.github.com/23maverick23/64b3b587c88697558fac*/ 46 | 47 | svg text { 48 | -webkit-user-select: none; 49 | -moz-user-select: none; 50 | -ms-user-select: none; 51 | user-select: none; 52 | } 53 | 54 | svg text::selection { 55 | background: none; 56 | } 57 | 58 | svg tspan { 59 | -webkit-user-select: none; 60 | -moz-user-select: none; 61 | -ms-user-select: none; 62 | user-select: none; 63 | } 64 | 65 | svg tspan::selection { 66 | background: none; 67 | } 68 | -------------------------------------------------------------------------------- /src/elm/View/SearchInputView.elm: -------------------------------------------------------------------------------- 1 | module View.SearchInputView exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Lazy as Lazy 6 | import Model.I18n as I18n exposing (Language) 7 | import View.Styles as S 8 | import Util.HtmlUtil as HtmlUtil 9 | 10 | 11 | view : Language -> (String -> msg) -> msg -> String -> Html msg 12 | view lang onInputMsg onSubmit query = 13 | HtmlUtil.form_ onSubmit 14 | [ style S.searchBoxContainer ] 15 | [ Lazy.lazy3 textInput lang onInputMsg query 16 | , Lazy.lazy submitButton lang 17 | ] 18 | 19 | 20 | textInput : Language -> (String -> msg) -> String -> Html msg 21 | textInput lang onInputMsg query = 22 | input 23 | [ id "search-box-input" 24 | , placeholder (I18n.searchPlaceHolder lang) 25 | , style S.searchBox 26 | , defaultValue query 27 | , HtmlUtil.onInput onInputMsg 28 | ] 29 | [] 30 | 31 | 32 | submitButton : Language -> Html msg 33 | submitButton lang = 34 | input 35 | [ type_ "submit" 36 | , style S.searchBoxSubmit 37 | , value (I18n.search lang) 38 | ] 39 | [] 40 | -------------------------------------------------------------------------------- /src/elm/Util/File.elm: -------------------------------------------------------------------------------- 1 | module Util.File exposing (..) 2 | 3 | import Native.File 4 | import Json.Decode exposing (..) 5 | import Task exposing (Task) 6 | 7 | 8 | type FileList 9 | = FileList Json.Decode.Value 10 | 11 | 12 | type File 13 | = File Json.Decode.Value 14 | 15 | 16 | type Error 17 | = Unexpected String 18 | 19 | 20 | readAsDataURL : File -> Task Error String 21 | readAsDataURL (File file) = 22 | Task.mapError 23 | (always (Unexpected (toString file))) 24 | (Native.File.readAsDataURL file) 25 | 26 | 27 | length : FileList -> Int 28 | length (FileList list) = 29 | Native.File.length list 30 | 31 | 32 | getAt : Int -> FileList -> Maybe File 33 | getAt index fileList = 34 | case fileList of 35 | FileList list -> 36 | if 0 <= index && index < length fileList then 37 | Just (File <| Native.File.getAt index list) 38 | else 39 | Nothing 40 | 41 | 42 | decodeFile : Decoder FileList 43 | decodeFile = 44 | Json.Decode.map FileList (at [ "target", "files" ] (value)) 45 | 46 | 47 | getSizeOfImage : String -> ( Int, Int ) 48 | getSizeOfImage = 49 | Native.File.getSizeOfImage 50 | -------------------------------------------------------------------------------- /src/elm/Component/ImageLoader.elm: -------------------------------------------------------------------------------- 1 | module Component.ImageLoader exposing (..) 2 | 3 | import Task 4 | import Html exposing (Html) 5 | import Util.File as File exposing (..) 6 | import Util.HtmlUtil as HtmlUtil 7 | import Model.I18n as I18n exposing (Language) 8 | import View.Styles as S 9 | 10 | 11 | type Msg 12 | = LoadFile FileList 13 | 14 | 15 | type alias Config msg = 16 | { onFileWithDataURL : File -> String -> msg 17 | , onFileLoadFailed : File.Error -> msg 18 | } 19 | 20 | 21 | update : Config msg -> Msg -> Cmd msg 22 | update config message = 23 | case message of 24 | LoadFile fileList -> 25 | File.getAt 0 fileList 26 | |> Maybe.map 27 | (\file -> 28 | readAsDataURL file 29 | |> Task.map (config.onFileWithDataURL file) 30 | |> Task.onError (Task.succeed << config.onFileLoadFailed) 31 | |> Task.perform identity 32 | ) 33 | |> Maybe.withDefault Cmd.none 34 | 35 | 36 | view : Language -> Html Msg 37 | view lang = 38 | HtmlUtil.fileLoadButton LoadFile S.imageLoadButton "image/*" (I18n.loadImage lang) 39 | -------------------------------------------------------------------------------- /server/static/generate-html.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var ejs = require('ejs'); 4 | var config = require('../lib/config.js'); 5 | 6 | var publicDir = __dirname + '/public'; 7 | 8 | var outputFiles = { 9 | index: path.join(publicDir, 'index.html'), 10 | login: path.join(publicDir, 'login.html'), 11 | master: path.join(publicDir, 'master.html') 12 | }; 13 | 14 | var templateDir = __dirname + '/template'; 15 | var indexHtml = ejs.render(fs.readFileSync(templateDir + '/index.html', 'utf8'), { 16 | apiRoot: config.apiRoot, 17 | accountServiceRoot: config.accountServiceRoot, 18 | title: config.title 19 | }); 20 | fs.writeFileSync(outputFiles.index, indexHtml); 21 | 22 | var loginHtml = ejs.render(fs.readFileSync(templateDir + '/login.html', 'utf8'), { 23 | accountServiceRoot: config.accountServiceRoot, 24 | title: config.title 25 | }); 26 | fs.writeFileSync(outputFiles.login, loginHtml); 27 | 28 | var masterHtml = ejs.render(fs.readFileSync(templateDir + '/master.html', 'utf8'), { 29 | apiRoot: config.apiRoot, 30 | accountServiceRoot: config.accountServiceRoot, 31 | title: config.title 32 | }); 33 | fs.writeFileSync(outputFiles.master, masterHtml); 34 | 35 | return outputFiles; 36 | -------------------------------------------------------------------------------- /docs/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Office Maker Demo 6 | 7 | 8 | 9 | 14 | 15 | 16 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/elm/View/DialogView.elm: -------------------------------------------------------------------------------- 1 | module View.DialogView exposing (viewWithSize, viewWithMarginParcentage) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Events exposing (..) 6 | import View.CommonStyles as S 7 | 8 | 9 | viewWithSize : msg -> Int -> Int -> Int -> List (Html msg) -> Html msg 10 | viewWithSize clickBackgroundMsg backgroundZIndex width height children = 11 | view clickBackgroundMsg backgroundZIndex (S.dialogWithSize (backgroundZIndex + 1) width height) children 12 | 13 | 14 | viewWithMarginParcentage : msg -> Int -> Int -> Int -> List (Html msg) -> Html msg 15 | viewWithMarginParcentage clickBackgroundMsg backgroundZIndex top left children = 16 | view clickBackgroundMsg backgroundZIndex (S.dialogWithMarginParcentage (backgroundZIndex + 1) top left) children 17 | 18 | 19 | view : msg -> Int -> List ( String, String ) -> List (Html msg) -> Html msg 20 | view clickBackgroundMsg backgroundZIndex dialogStyles children = 21 | div [] 22 | [ div 23 | [ style (S.modalBackground backgroundZIndex) 24 | , onClick clickBackgroundMsg 25 | ] 26 | [] 27 | , div 28 | [ style dialogStyles ] 29 | children 30 | ] 31 | -------------------------------------------------------------------------------- /server/lib/mock.js: -------------------------------------------------------------------------------- 1 | var gridSize = 8; 2 | var backgroundColors = [ 3 | "#eee", "#edb", "#bbf", "#fbb", "#abe", "#af9", "#9df", "#bbb", "#fff", "rgba(255,255,255,0.5)" 4 | ]; 5 | var colors = [ 6 | "#875", "#75a", "#c57", "#69a", "#8c5", "#5ab", "#666", "#000" 7 | ]; 8 | var prototypes = [{ 9 | id: "1", 10 | name: "", 11 | width: gridSize * 7, //70cm 12 | height: gridSize * 12, //120cm 13 | backgroundColor: "#eee", 14 | color: "#000", 15 | fontSize: 20, 16 | shape: 'rectangle' 17 | }, { 18 | id: "2", 19 | name: "", 20 | width: gridSize * 12, //120cm 21 | height: gridSize * 7, //70cm 22 | backgroundColor: "#eee", 23 | color: "#000", 24 | fontSize: 20, 25 | shape: 'rectangle' 26 | }]; 27 | var allColors = backgroundColors.map((c, index) => { 28 | var id = index + ''; 29 | var ord = index; 30 | return { 31 | id: id, 32 | ord: ord, 33 | type: 'backgroundColor', 34 | color: c 35 | }; 36 | }).concat(colors.map((c, index) => { 37 | var id = (backgroundColors.length + index) + ''; 38 | var ord = (backgroundColors.length + index); 39 | return { 40 | id: id, 41 | ord: ord, 42 | type: 'color', 43 | color: c 44 | }; 45 | })); 46 | module.exports = { 47 | colors: allColors, 48 | prototypes: prototypes 49 | }; 50 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src/elm", 8 | "persistent-cache/src" 9 | ], 10 | "exposed-modules": [], 11 | "native-modules": true, 12 | "dependencies": { 13 | "NoRedInk/elm-decode-pipeline": "3.0.0 <= v < 4.0.0", 14 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 15 | "elm-lang/dom": "1.0.0 <= v < 2.0.0", 16 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 17 | "elm-lang/http": "1.0.0 <= v < 2.0.0", 18 | "elm-lang/keyboard": "1.0.0 <= v < 2.0.0", 19 | "elm-lang/mouse": "1.0.0 <= v < 2.0.0", 20 | "elm-lang/navigation": "2.0.0 <= v < 3.0.0", 21 | "elm-lang/svg": "2.0.0 <= v < 3.0.0", 22 | "elm-lang/virtual-dom": "2.0.0 <= v < 3.0.0", 23 | "elm-lang/window": "1.0.0 <= v < 2.0.0", 24 | "jinjor/elm-contextmenu": "1.0.2 <= v < 2.0.0", 25 | "jinjor/elm-debounce": "2.0.0 <= v < 3.0.0", 26 | "jinjor/elm-html-parser": "1.0.2 <= v < 2.0.0", 27 | "jinjor/elm-inline-hover": "1.0.0 <= v < 2.0.0", 28 | "jystic/elm-font-awesome": "2.0.0 <= v < 3.0.0" 29 | }, 30 | "elm-version": "0.18.0 <= v < 0.19.0" 31 | } 32 | -------------------------------------------------------------------------------- /src/elm/CoreType.elm: -------------------------------------------------------------------------------- 1 | module CoreType exposing (..) 2 | 3 | import Json.Encode 4 | 5 | 6 | type alias Position = 7 | { x : Int 8 | , y : Int 9 | } 10 | 11 | 12 | type alias PositionFloat = 13 | { x : Float 14 | , y : Float 15 | } 16 | 17 | 18 | type alias Size = 19 | { width : Int 20 | , height : Int 21 | } 22 | 23 | 24 | type alias Id = 25 | String 26 | 27 | 28 | type alias ObjectId = 29 | Id 30 | 31 | 32 | type alias PersonId = 33 | Id 34 | 35 | 36 | type alias FloorId = 37 | Id 38 | 39 | 40 | type alias Json = 41 | Json.Encode.Value 42 | 43 | 44 | 45 | -- DIRECTION 46 | 47 | 48 | type Direction 49 | = Up 50 | | Left 51 | | Right 52 | | Down 53 | 54 | 55 | opposite : Direction -> Direction 56 | opposite direction = 57 | case direction of 58 | Left -> 59 | Right 60 | 61 | Right -> 62 | Left 63 | 64 | Up -> 65 | Down 66 | 67 | Down -> 68 | Up 69 | 70 | 71 | shiftTowards : Direction -> number -> ( number, number ) 72 | shiftTowards direction amount = 73 | case direction of 74 | Up -> 75 | ( 0, -amount ) 76 | 77 | Down -> 78 | ( 0, amount ) 79 | 80 | Right -> 81 | ( amount, 0 ) 82 | 83 | Left -> 84 | ( -amount, 0 ) 85 | -------------------------------------------------------------------------------- /src/elm/Native/ClipboardData.js: -------------------------------------------------------------------------------- 1 | var _user$project$Native_ClipboardData = function(localRuntime) { 2 | function getHtml(clipboardData) { 3 | return clipboardData.getData('text/html'); 4 | } 5 | 6 | function getText(clipboardData) { 7 | return clipboardData.getData('text'); 8 | } 9 | 10 | function execCopy(s) { 11 | var textArea = document.createElement("textarea"); 12 | textArea.value = s; 13 | document.body.appendChild(textArea); 14 | textArea.select(); 15 | var result = document.execCommand("copy"); 16 | document.body.removeChild(textArea); 17 | }; 18 | return { 19 | execCopy: execCopy, 20 | getHtml: getHtml, 21 | getText: getText 22 | }; 23 | }(); 24 | 25 | 26 | // var items = clipboardData.items; 27 | // for (var i = 0 ; i < items.length ; i++) { 28 | // var item = items[i]; 29 | // console.log(item.type); 30 | // if (item.type.indexOf("image") != -1) { 31 | // var file = item.getAsFile(); 32 | // console.log(file); 33 | // } 34 | // } 35 | // console.log(clipboardData.types); 36 | // console.log(clipboardData.files[0]); 37 | // console.log(clipboardData.items[0]); 38 | // console.log(clipboardData.getData('text')); 39 | // console.log(clipboardData.getData('text/html')); 40 | // console.log(clipboardData.getData('text/rtf')); 41 | // console.log(clipboardData.getData('text/plain')); 42 | // console.log(clipboardData.getData('Files')); 43 | // console.log(clipboardData.getData('image/png')); 44 | -------------------------------------------------------------------------------- /src/elm/View/Common.elm: -------------------------------------------------------------------------------- 1 | module View.Common exposing (card, formControl) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import View.CommonStyles exposing (..) 6 | import Util.StyleUtil exposing (..) 7 | 8 | 9 | card : Bool -> String -> Maybe Int -> Maybe Int -> List (Html msg) -> Html msg 10 | card absolute backgroundColor maxHeight maybeWidth children = 11 | div 12 | [ style (cardStyles absolute backgroundColor maxHeight maybeWidth) ] 13 | children 14 | 15 | 16 | cardStyles : Bool -> String -> Maybe Int -> Maybe Int -> S 17 | cardStyles absolute backgroundColor maybeMaxHeight maybeWidth = 18 | (case maybeMaxHeight of 19 | Just maxHeight -> 20 | [ ( "max-height", px maxHeight ) 21 | , ( "overflow-y", "scroll" ) 22 | , ( "box-sizing", "border-box" ) 23 | ] 24 | 25 | Nothing -> 26 | [] 27 | ) 28 | ++ [ ( "background-color", backgroundColor ) 29 | , ( "width", maybeWidth |> Maybe.map px |> Maybe.withDefault "" ) 30 | , ( "position" 31 | , if absolute then 32 | "absolute" 33 | else 34 | "" 35 | ) 36 | , ( "z-index" 37 | , if absolute then 38 | "1" 39 | else 40 | "" 41 | ) 42 | ] 43 | ++ View.CommonStyles.card 44 | 45 | 46 | formControl : List (Html msg) -> Html msg 47 | formControl children = 48 | div [ style View.CommonStyles.formControl ] children 49 | -------------------------------------------------------------------------------- /server/sql/2-create-tables.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE map2.floors ( 2 | id VARCHAR(36) NOT NULL, 3 | tenantId VARCHAR(64), 4 | version INT NOT NULL, 5 | name VARCHAR(128) NOT NULL, 6 | ord INT NOT NULL, 7 | image VARCHAR(128), 8 | width INT NOT NULL, 9 | height INT NOT NULL, 10 | realWidth INT, 11 | realHeight INT, 12 | public BOOLEAN, 13 | updateBy VARCHAR(128), 14 | updateAt BIGINT, 15 | UNIQUE(id, version) 16 | ); 17 | 18 | CREATE TABLE map2.objects ( 19 | id VARCHAR(36) NOT NULL, 20 | type VARCHAR(16) NOT NULL, 21 | name VARCHAR(128) NOT NULL, 22 | x INT NOT NULL, 23 | y INT NOT NULL, 24 | width INT NOT NULL, 25 | height INT NOT NULL, 26 | backgroundColor VARCHAR(64) NOT NULL, 27 | color VARCHAR(64) NOT NULL, 28 | fontSize DECIMAL(4,1) NOT NULL, 29 | shape VARCHAR(64) NOT NULL, 30 | modifiedVersion INT(11) NOT NULL, 31 | personId VARCHAR(128), 32 | floorId VARCHAR(36) NOT NULL, 33 | floorVersion INT NOT NULL, 34 | UNIQUE(id, floorId, floorVersion) 35 | ); 36 | 37 | CREATE TABLE map2.prototypes ( 38 | id VARCHAR(36) NOT NULL PRIMARY KEY, 39 | tenantId VARCHAR(64) NOT NULL, 40 | name VARCHAR(128) NOT NULL, 41 | width INT NOT NULL, 42 | height INT NOT NULL, 43 | backgroundColor VARCHAR(64) NOT NULL, 44 | color VARCHAR(64) NOT NULL, 45 | fontSize DECIMAL(4,1) NOT NULL, 46 | shape VARCHAR(64) NOT NULL 47 | ); 48 | 49 | CREATE TABLE map2.colors ( 50 | id VARCHAR(36) NOT NULL PRIMARY KEY, 51 | tenantId VARCHAR(64) NOT NULL, 52 | ord INT NOT NULL, 53 | type VARCHAR(16) NOT NULL, 54 | color VARCHAR(64) NOT NULL 55 | ); 56 | -------------------------------------------------------------------------------- /src/elm/Util/ListUtil.elm: -------------------------------------------------------------------------------- 1 | module Util.ListUtil exposing (..) 2 | 3 | 4 | sepBy : Int -> List a -> List (List a) 5 | sepBy count list = 6 | case List.drop count list of 7 | [] -> 8 | [ List.take count list ] 9 | 10 | cont -> 11 | List.take count list :: sepBy count cont 12 | 13 | 14 | findBy : (a -> Bool) -> List a -> Maybe a 15 | findBy f list = 16 | List.head (List.filter f list) 17 | 18 | 19 | zipWithIndex : List a -> List ( a, Int ) 20 | zipWithIndex = 21 | zipWithIndexFrom 0 22 | 23 | 24 | zipWithIndexFrom : Int -> List a -> List ( a, Int ) 25 | zipWithIndexFrom index list = 26 | case list of 27 | h :: t -> 28 | ( h, index ) :: zipWithIndexFrom (index + 1) t 29 | 30 | _ -> 31 | [] 32 | 33 | 34 | getAt : Int -> List a -> Maybe a 35 | getAt index xs = 36 | if index < 0 then 37 | Nothing 38 | else 39 | List.head <| List.drop index xs 40 | 41 | 42 | setAt : Int -> a -> List a -> List a 43 | setAt index value list = 44 | case list of 45 | head :: tail -> 46 | if index == 0 then 47 | value :: tail 48 | else 49 | head :: setAt (index - 1) value tail 50 | 51 | [] -> 52 | list 53 | 54 | 55 | deleteAt : Int -> List a -> List a 56 | deleteAt index list = 57 | case list of 58 | head :: tail -> 59 | if index == 0 then 60 | tail 61 | else 62 | head :: deleteAt (index - 1) tail 63 | 64 | [] -> 65 | list 66 | -------------------------------------------------------------------------------- /src/elm/Page/Map/GridLayer.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.GridLayer exposing (view) 2 | 3 | import Svg exposing (..) 4 | import Svg.Attributes exposing (..) 5 | import Svg.Lazy exposing (..) 6 | import Model.Scale as Scale exposing (Scale) 7 | import Model.Floor as Floor exposing (Floor) 8 | import Page.Map.Model as Model exposing (Model) 9 | 10 | 11 | view : Model -> Floor -> Svg msg 12 | view model floor = 13 | lazy3 viewHelp model.scale model.gridSize floor 14 | 15 | 16 | viewHelp : Scale -> Int -> Floor -> Svg msg 17 | viewHelp scale gridSize floor = 18 | let 19 | intervalPxOnScreen = 20 | 50 * gridSize 21 | 22 | width = 23 | Floor.width floor 24 | 25 | height = 26 | Floor.height floor 27 | 28 | lefts = 29 | List.map ((*) intervalPxOnScreen) (List.range 1 (width // intervalPxOnScreen)) 30 | 31 | tops = 32 | List.map ((*) intervalPxOnScreen) (List.range 1 (height // intervalPxOnScreen)) 33 | 34 | vertical = 35 | List.map (lazy2 verticalLine height) lefts 36 | 37 | horizontal = 38 | List.map (lazy2 horizontalLine width) tops 39 | in 40 | g [] (vertical ++ horizontal) 41 | 42 | 43 | verticalLine : Int -> Int -> Svg msg 44 | verticalLine height left = 45 | Svg.path [ strokeDasharray "5,5", stroke "black", d ("M" ++ toString left ++ ",0V" ++ toString height) ] [] 46 | 47 | 48 | horizontalLine : Int -> Int -> Svg msg 49 | horizontalLine width top = 50 | Svg.path [ strokeDasharray "5,5", stroke "black", d ("M0," ++ toString top ++ "H" ++ toString width) ] [] 51 | -------------------------------------------------------------------------------- /src/elm/View/MessageBarForMainView.elm: -------------------------------------------------------------------------------- 1 | module View.MessageBarForMainView exposing (view) 2 | 3 | import Http 4 | import Html exposing (..) 5 | import View.MessageBar as MessageBar 6 | import Model.I18n as I18n exposing (Language) 7 | import Model.Errors exposing (GlobalError(..)) 8 | import API.API as API 9 | 10 | 11 | view : Language -> GlobalError -> Html msg 12 | view lang e = 13 | case e of 14 | NoError -> 15 | MessageBar.none 16 | 17 | Success message -> 18 | MessageBar.success message 19 | 20 | APIError e -> 21 | MessageBar.error (describeAPIError lang e) 22 | 23 | FileError e -> 24 | MessageBar.error (I18n.unexpectedFileError lang ++ ": " ++ toString e) 25 | 26 | HtmlError e -> 27 | MessageBar.error (I18n.unexpectedHtmlError lang ++ ": " ++ toString e) 28 | 29 | PasteError s -> 30 | MessageBar.error s 31 | 32 | 33 | describeAPIError : Language -> API.Error -> String 34 | describeAPIError lang e = 35 | case e of 36 | Http.BadUrl url -> 37 | I18n.unexpectedBadUrl lang ++ ": " ++ url 38 | 39 | Http.Timeout -> 40 | I18n.timeout lang 41 | 42 | Http.NetworkError -> 43 | I18n.networkErrorDetectedPleaseRefreshAndTryAgain lang 44 | 45 | Http.BadStatus res -> 46 | if res.status.code == 409 then 47 | I18n.conflictSomeoneHasAlreadyChangedPleaseRefreshAndTryAgain lang 48 | else 49 | I18n.unexpectedBadStatus lang ++ ": " ++ toString res.status.code ++ " " ++ res.status.message 50 | 51 | Http.BadPayload str res -> 52 | I18n.unexpectedPayload lang ++ ": " ++ str 53 | 54 | 55 | 56 | -- 57 | -------------------------------------------------------------------------------- /server/lib/sql.js: -------------------------------------------------------------------------------- 1 | var mysql = require('mysql'); 2 | var esc = mysql.escape.bind(mysql); 3 | 4 | function select(table, where) { 5 | return `SELECT * FROM ${table}` + (where ? ` ${where}` : ''); 6 | } 7 | 8 | function insert(table, keyValues) { 9 | var columns = []; 10 | var values = []; 11 | keyValues.forEach((keyValue) => { 12 | columns.push(keyValue[0]); 13 | values.push(esc(keyValue[1])); 14 | }); 15 | var columnsStr = `(${ columns.join(',') })`; 16 | var valuesStr = `VALUES(${ values.join(',') })`; 17 | return `INSERT INTO ${table} ${columnsStr} ${valuesStr}`; 18 | } 19 | 20 | function replace(table, keyValues) { 21 | var columns = []; 22 | var values = []; 23 | keyValues.forEach((keyValue) => { 24 | columns.push(keyValue[0]); 25 | values.push(esc(keyValue[1])); 26 | }); 27 | var columnsStr = `(${ columns.join(',') })`; 28 | var valuesStr = `VALUES(${ values.join(',') })`; 29 | return `REPLACE INTO ${table} ${columnsStr} ${valuesStr}`; 30 | } 31 | 32 | function update(table, keyValues, where) { 33 | var sets = keyValues.map((keyValue) => { 34 | return `${ keyValue[0] }=${ esc(keyValue[1]) }`; 35 | }); 36 | var str = `SET ${ sets.join(',') }`; 37 | return `UPDATE ${ table } ${ str } ${ where }`; 38 | } 39 | 40 | function where(key, value) { 41 | return `WHERE ${key}=${esc(value)}`; 42 | } 43 | 44 | function whereList(keyValues) { 45 | return `WHERE ` + keyValues.map((keyValue) => { 46 | return `${keyValue[0]}=${esc(keyValue[1])}`; 47 | }).join(' AND '); 48 | } 49 | 50 | function _delete(table, where) { 51 | return `DELETE FROM ${ table }` + (where ? ` ${where}` : ''); 52 | } 53 | module.exports = { 54 | select: select, 55 | insert: insert, 56 | replace: replace, 57 | update: update, 58 | delete: _delete, 59 | where: where, 60 | whereList: whereList 61 | }; 62 | -------------------------------------------------------------------------------- /src/elm/Model/SaveRequest.elm: -------------------------------------------------------------------------------- 1 | module Model.SaveRequest exposing (..) 2 | 3 | import Model.Floor exposing (Floor) 4 | import Model.EditingFloor as EditingFloor exposing (EditingFloor) 5 | import Model.ObjectsChange as ObjectsChange exposing (ObjectsChange) 6 | import CoreType exposing (..) 7 | 8 | 9 | type SaveRequest 10 | = SaveFloor Floor 11 | | PublishFloor FloorId 12 | | SaveObjects ObjectsChange 13 | 14 | 15 | type alias ReducedSaveRequest = 16 | { floor : Maybe Floor 17 | , publish : Maybe FloorId 18 | , objects : ObjectsChange 19 | } 20 | 21 | 22 | emptyReducedSaveRequest : ReducedSaveRequest 23 | emptyReducedSaveRequest = 24 | { floor = Nothing 25 | , publish = Nothing 26 | , objects = ObjectsChange.empty 27 | } 28 | 29 | 30 | reduceRequest : Maybe EditingFloor -> List SaveRequest -> ReducedSaveRequest 31 | reduceRequest maybeEditingFloor list = 32 | case maybeEditingFloor of 33 | Just efloor -> 34 | List.foldr (reduceRequestHelp efloor) emptyReducedSaveRequest list 35 | 36 | Nothing -> 37 | emptyReducedSaveRequest 38 | 39 | 40 | reduceRequestHelp : EditingFloor -> SaveRequest -> ReducedSaveRequest -> ReducedSaveRequest 41 | reduceRequestHelp efloor req reducedSaveRequest = 42 | case req of 43 | SaveFloor floor -> 44 | { reducedSaveRequest 45 | | floor = Just floor 46 | } 47 | 48 | PublishFloor floorId -> 49 | { reducedSaveRequest 50 | | publish = Just floorId 51 | } 52 | 53 | SaveObjects objectsChange -> 54 | { reducedSaveRequest 55 | | objects = 56 | ObjectsChange.merge 57 | (EditingFloor.present efloor).objects 58 | objectsChange 59 | reducedSaveRequest.objects 60 | } 61 | -------------------------------------------------------------------------------- /persistent-cache/src/LocalStorage.elm: -------------------------------------------------------------------------------- 1 | module LocalStorage 2 | exposing 3 | ( get 4 | , set 5 | , remove 6 | , clear 7 | , keys 8 | , Error(..) 9 | ) 10 | 11 | {-| Low-level bindings to the [localStorage] API. 12 | -} 13 | 14 | import Native.LocalStorage 15 | import Task exposing (Task) 16 | 17 | 18 | {-| These low-level operations can fail in a few ways: 19 | 20 | - `QuotaExceeded` means you exceeded your 5mb and need to `clear` or `remove` 21 | some information to make more space. 22 | - `Disabled` means the user turned off local storage. It is rare, but it can 23 | happen. 24 | 25 | -} 26 | type Error 27 | = QuotaExceeded 28 | | Disabled 29 | 30 | 31 | {-| Get the value at a particular key. 32 | 33 | get "age" 34 | 35 | -} 36 | get : String -> Task Error (Maybe String) 37 | get = 38 | Native.LocalStorage.get 39 | 40 | 41 | {-| Set a key to a particular value. If the key does not exist, it is added. 42 | If the key already exists, we overwrite the old data. 43 | 44 | set "age" "42" 45 | 46 | Most browsers cap you at 5MB of space, so this can trigger a `QuotaExceeded` 47 | error if you are adding enough data to cross that threshold. 48 | 49 | -} 50 | set : String -> String -> Task Error () 51 | set = 52 | Native.LocalStorage.set 53 | 54 | 55 | {-| Remove a particular key and its corresponding value. 56 | 57 | remove "age" 58 | 59 | -} 60 | remove : String -> Task Error () 61 | remove = 62 | Native.LocalStorage.remove 63 | 64 | 65 | {-| Remove everything in local storage. 66 | -} 67 | clear : Task Error () 68 | clear = 69 | Native.LocalStorage.clear 70 | 71 | 72 | {-| Get all the keys currently stored. So if you `set` two entries named 73 | `"draft"` and `"title"`, running the `keys` task will produce 74 | `["draft", "title"]`. If you have not `set` any entries yet, you will get `[]`. 75 | -} 76 | keys : Task Error (List String) 77 | keys = 78 | Native.LocalStorage.keys 79 | -------------------------------------------------------------------------------- /src/elm/Model/ColorPalette.elm: -------------------------------------------------------------------------------- 1 | module Model.ColorPalette exposing (..) 2 | 3 | import Util.ListUtil as ListUtil 4 | 5 | 6 | type alias ColorPalette = 7 | { backgroundColors : List String 8 | , textColors : List String 9 | } 10 | 11 | 12 | empty : ColorPalette 13 | empty = 14 | { backgroundColors = [] 15 | , textColors = [] 16 | } 17 | 18 | 19 | setTextColorAt : Int -> String -> ColorPalette -> ColorPalette 20 | setTextColorAt index color colorPalette = 21 | { colorPalette 22 | | textColors = ListUtil.setAt index color colorPalette.textColors 23 | } 24 | 25 | 26 | setBackgroundColorAt : Int -> String -> ColorPalette -> ColorPalette 27 | setBackgroundColorAt index color colorPalette = 28 | { colorPalette 29 | | backgroundColors = ListUtil.setAt index color colorPalette.backgroundColors 30 | } 31 | 32 | 33 | addTextColorToLast : String -> ColorPalette -> ColorPalette 34 | addTextColorToLast color colorPalette = 35 | { colorPalette 36 | | textColors = 37 | colorPalette.textColors 38 | |> List.reverse 39 | |> ((::) color) 40 | |> List.reverse 41 | } 42 | 43 | 44 | addBackgroundColorToLast : String -> ColorPalette -> ColorPalette 45 | addBackgroundColorToLast color colorPalette = 46 | { colorPalette 47 | | backgroundColors = 48 | colorPalette.backgroundColors 49 | |> List.reverse 50 | |> ((::) color) 51 | |> List.reverse 52 | } 53 | 54 | 55 | deleteTextColorAt : Int -> ColorPalette -> ColorPalette 56 | deleteTextColorAt index colorPalette = 57 | { colorPalette 58 | | textColors = ListUtil.deleteAt index colorPalette.textColors 59 | } 60 | 61 | 62 | deleteBackgroundColorAt : Int -> ColorPalette -> ColorPalette 63 | deleteBackgroundColorAt index colorPalette = 64 | { colorPalette 65 | | backgroundColors = ListUtil.deleteAt index colorPalette.backgroundColors 66 | } 67 | -------------------------------------------------------------------------------- /src/elm/Page/Map/FloorUpdateInfoView.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.FloorUpdateInfoView exposing (view) 2 | 3 | import Dict 4 | import Date exposing (Date) 5 | import Html exposing (..) 6 | import Html.Attributes exposing (..) 7 | import Html.Lazy as Lazy 8 | import View.Styles as S 9 | import Model.DateFormatter as DateFormatter 10 | import Model.I18n as I18n exposing (Language) 11 | import Model.EditingFloor as EditingFloor 12 | import Model.Mode as Mode 13 | import Page.Map.Model exposing (Model) 14 | 15 | 16 | view : Model -> Html msg 17 | view model = 18 | model.floor 19 | |> Maybe.map EditingFloor.present 20 | |> Maybe.andThen (\floor -> floor.update) 21 | |> Maybe.map 22 | (\{ by, at } -> 23 | let 24 | name = 25 | Dict.get by model.personInfo 26 | |> Maybe.map .name 27 | |> Maybe.withDefault by 28 | in 29 | viewHelp model.lang model.visitDate name at (Mode.isPrintMode model.mode) 30 | ) 31 | |> Maybe.withDefault (text "") 32 | 33 | 34 | viewHelp : Language -> Date -> String -> Date -> Bool -> Html msg 35 | viewHelp lang visitDate by at printMode = 36 | Lazy.lazy2 37 | viewHelpHelp 38 | printMode 39 | (I18n.lastUpdateByAt lang by (formatDate lang printMode visitDate at)) 40 | 41 | 42 | formatDate : Language -> Bool -> Date -> Date -> String 43 | formatDate lang printMode visitDate at = 44 | if printMode then 45 | DateFormatter.formatDate lang at 46 | else 47 | DateFormatter.formatDateOrTime lang visitDate at 48 | 49 | 50 | viewHelpHelp : Bool -> String -> Html msg 51 | viewHelpHelp printMode string = 52 | div 53 | [ style 54 | (if printMode then 55 | S.floorPropertyLastUpdateForPrint 56 | else 57 | S.floorPropertyLastUpdate 58 | ) 59 | ] 60 | [ text string ] 61 | -------------------------------------------------------------------------------- /watch.js: -------------------------------------------------------------------------------- 1 | const cp = require('child_process'); 2 | const watch = require('watch'); 3 | const path = require('path'); 4 | const slash = require('slash'); 5 | const minimatch = require('minimatch'); 6 | 7 | var runServer = !process.argv.filter(a => { 8 | return a == '--no-server' 9 | }).length; 10 | var debugMode = process.argv.filter(a => { 11 | return a == '--debug' 12 | }).length; 13 | var server = null; 14 | 15 | var queued = { 16 | build: false, 17 | server: false 18 | }; 19 | 20 | function taskBuild(cb) { 21 | if (queued.build) { 22 | queued.build = false; 23 | // console.log('build start\n'); 24 | var args = ['build.sh']; 25 | if (debugMode) { 26 | args.push('--debug'); 27 | } 28 | var sh = cp.spawn('sh', args, { 29 | stdio: 'inherit' 30 | }); 31 | sh.on('close', cb); 32 | } else { 33 | cb(); 34 | } 35 | } 36 | 37 | function taskServer(cb) { 38 | if (runServer && queued.server) { 39 | queued.server = false; 40 | server && server.kill(); 41 | server = cp.spawn('node', ['server/server'], { 42 | stdio: 'inherit' 43 | }); 44 | cb(); 45 | } else { 46 | cb(); 47 | } 48 | } 49 | 50 | function run() { 51 | taskBuild(() => { 52 | taskServer(() => { 53 | setTimeout(run, 300); 54 | }); 55 | }); 56 | } 57 | 58 | function schedule(type, stat) { 59 | queued[type] = true; 60 | } 61 | watch.createMonitor('src', (monitor) => { 62 | monitor.on("created", schedule.bind(null, 'build')); 63 | monitor.on("changed", schedule.bind(null, 'build')); 64 | monitor.on("removed", schedule.bind(null, 'build')); 65 | }); 66 | 67 | watch.createMonitor('server', { 68 | filter: function(file) { 69 | return !file.includes('public') && !file.includes('node_modules'); 70 | } 71 | }, (monitor) => { 72 | monitor.on("created", schedule.bind(null, 'server')); 73 | monitor.on("changed", schedule.bind(null, 'server')); 74 | monitor.on("removed", schedule.bind(null, 'server')); 75 | }); 76 | 77 | schedule('build'); 78 | schedule('server'); 79 | run(); 80 | -------------------------------------------------------------------------------- /src/elm/Util/UUID.elm: -------------------------------------------------------------------------------- 1 | module Util.UUID exposing (Seed, init, step) 2 | 3 | -- TODO maybe this implementation is incorrect. Use library instead. 4 | 5 | import Random 6 | 7 | 8 | type Seed 9 | = Seed Random.Seed Random.Seed 10 | 11 | 12 | r1 : Random.Generator String 13 | r1 = 14 | Random.map toHex <| Random.int 0 15 15 | 16 | 17 | r2 : Random.Generator String 18 | r2 = 19 | Random.map toHex <| Random.int 8 11 20 | 21 | 22 | toHex : Int -> String 23 | toHex i = 24 | if i == 0 then 25 | "0" 26 | else if i == 1 then 27 | "1" 28 | else if i == 2 then 29 | "2" 30 | else if i == 3 then 31 | "3" 32 | else if i == 4 then 33 | "4" 34 | else if i == 5 then 35 | "5" 36 | else if i == 6 then 37 | "6" 38 | else if i == 7 then 39 | "7" 40 | else if i == 8 then 41 | "8" 42 | else if i == 9 then 43 | "9" 44 | else if i == 10 then 45 | "a" 46 | else if i == 11 then 47 | "b" 48 | else if i == 12 then 49 | "c" 50 | else if i == 13 then 51 | "d" 52 | else if i == 14 then 53 | "e" 54 | else if i == 15 then 55 | "f" 56 | else 57 | Debug.crash "" 58 | 59 | 60 | f : Char -> ( String, Random.Seed, Random.Seed ) -> ( String, Random.Seed, Random.Seed ) 61 | f c ( s, s1, s2 ) = 62 | if c == 'x' then 63 | let 64 | ( c_, s1_ ) = 65 | Random.step r1 s1 66 | in 67 | ( s ++ c_, s1_, s2 ) 68 | else if c == 'y' then 69 | let 70 | ( c_, s2_ ) = 71 | Random.step r2 s2 72 | in 73 | ( s ++ c_, s1, s2_ ) 74 | else 75 | ( s ++ String.fromChar c, s1, s2 ) 76 | 77 | 78 | init : Int -> Int -> Seed 79 | init i1 i2 = 80 | Seed (Random.initialSeed i1) (Random.initialSeed i2) 81 | 82 | 83 | step : Seed -> ( String, Seed ) 84 | step (Seed s1 s2) = 85 | let 86 | ( s, s1_, s2_ ) = 87 | List.foldl f ( "", s1, s2 ) (String.toList "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx") 88 | in 89 | ( s, Seed s1_ s2_ ) 90 | -------------------------------------------------------------------------------- /src/elm/View/PrototypePreviewView.elm: -------------------------------------------------------------------------------- 1 | module View.PrototypePreviewView exposing (view, singleView, emptyView) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Svg exposing (Svg) 6 | import Svg.Attributes 7 | import View.Styles as S 8 | import View.ObjectView as ObjectView 9 | import Model.Scale as Scale exposing (Scale) 10 | import Model.Prototype exposing (Prototype) 11 | import CoreType exposing (..) 12 | 13 | 14 | view : Size -> Int -> List Prototype -> Html msg 15 | view containerSize selectedIndex prototypes = 16 | let 17 | inner = 18 | Svg.svg 19 | [ Svg.Attributes.width (toString <| containerSize.width * 4) 20 | , Svg.Attributes.height (toString containerSize.height) 21 | , style (S.prototypePreviewViewInner containerSize selectedIndex) 22 | ] 23 | (List.indexedMap (eachView containerSize selectedIndex) prototypes) 24 | in 25 | div [ style (S.prototypePreviewView containerSize.width containerSize.height) ] [ inner ] 26 | 27 | 28 | singleView : Size -> Prototype -> Html msg 29 | singleView containerSize prototype = 30 | view containerSize 0 [ prototype ] 31 | 32 | 33 | emptyView : Size -> Html msg 34 | emptyView containerSize = 35 | view containerSize 0 [] 36 | 37 | 38 | eachView : Size -> Int -> Int -> Prototype -> Html msg 39 | eachView containerSize selectedIndex index prototype = 40 | let 41 | selected = 42 | selectedIndex == index 43 | 44 | left = 45 | containerSize.width // 2 - prototype.width // 2 + index * containerSize.width 46 | 47 | top = 48 | containerSize.height // 2 - prototype.height // 2 49 | in 50 | ObjectView.viewDesk 51 | ObjectView.noEvents 52 | False 53 | (Position left top) 54 | (Size prototype.width prototype.height) 55 | prototype.backgroundColor 56 | prototype.name 57 | --name 58 | prototype.fontSize 59 | False 60 | -- selected 61 | False 62 | -- alpha 63 | (Scale.init 0) 64 | False 65 | 66 | 67 | 68 | -- personMatched 69 | -------------------------------------------------------------------------------- /persistent-cache/src/Native/LocalStorage.js: -------------------------------------------------------------------------------- 1 | var _user$project$Native_LocalStorage = function() { 2 | 3 | if (!localStorage || !localStorage.getItem || !localStorage.setItem) { 4 | function disabled() { 5 | return _elm_lang$core$Native_Scheduler.fail({ 6 | ctor: 'Disabled' 7 | }); 8 | } 9 | 10 | return { 11 | get: disabled, 12 | set: F2(disabled), 13 | remove: disabled, 14 | clear: disabled(), 15 | keys: disabled() 16 | }; 17 | } 18 | 19 | function get(key) { 20 | return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) { 21 | var value = localStorage.getItem(key); 22 | callback(_elm_lang$core$Native_Scheduler.succeed( 23 | value === null ? 24 | _elm_lang$core$Maybe$Nothing : 25 | _elm_lang$core$Maybe$Just(value) 26 | )); 27 | }); 28 | } 29 | 30 | function set(key, value) { 31 | return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) { 32 | try { 33 | localStorage.setItem(key, value); 34 | return callback(_elm_lang$core$Native_Scheduler.succeed(_elm_lang$core$Native_Utils.Tuple0)); 35 | } catch (e) { 36 | return callback(_elm_lang$core$Native_Scheduler.fail({ 37 | ctor: 'QuotaExceeded' 38 | })); 39 | } 40 | }); 41 | } 42 | 43 | function remove(key) { 44 | return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) { 45 | localStorage.removeItem(key); 46 | callback(_elm_lang$core$Native_Scheduler.succeed(_elm_lang$core$Native_Utils.Tuple0)); 47 | }); 48 | } 49 | 50 | var clear = _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) { 51 | localStorage.clear(); 52 | callback(_elm_lang$core$Native_Scheduler.succeed(_elm_lang$core$Native_Utils.Tuple0)); 53 | }); 54 | 55 | var keys = _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) { 56 | var keyList = _elm_lang$core$Native_List.Nil; 57 | for (var i = localStorage.length; i--;) { 58 | keyList = _elm_lang$core$Native_List.Cons(localStorage.key(i), keyList); 59 | } 60 | callback(_elm_lang$core$Native_Scheduler.succeed(keyList)); 61 | }); 62 | 63 | return { 64 | get: get, 65 | set: F2(set), 66 | remove: remove, 67 | clear: clear, 68 | keys: keys 69 | }; 70 | 71 | }(); 72 | -------------------------------------------------------------------------------- /src/elm/Component/Dialog.elm: -------------------------------------------------------------------------------- 1 | module Component.Dialog exposing (..) 2 | 3 | import Task 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import View.DialogView as DialogView 8 | import View.CommonStyles as S 9 | 10 | 11 | type alias Config msg = 12 | { strategy : Strategy msg 13 | , transform : Msg msg -> msg 14 | } 15 | 16 | 17 | type Strategy msg 18 | = ConfirmOrClose ( String, msg ) ( String, msg ) 19 | 20 | 21 | type alias Dialog = 22 | Bool 23 | 24 | 25 | type Msg msg 26 | = Open 27 | | Confirm msg 28 | | Close msg 29 | 30 | 31 | init : Dialog 32 | init = 33 | False 34 | 35 | 36 | update : Msg msg -> Dialog -> ( Dialog, Cmd msg ) 37 | update msg model = 38 | case msg of 39 | Open -> 40 | True ! [] 41 | 42 | Close msg -> 43 | False ! [ Task.perform identity <| Task.succeed msg ] 44 | 45 | Confirm msg -> 46 | False ! [ Task.perform identity <| Task.succeed msg ] 47 | 48 | 49 | open : (Msg msg -> msg) -> Cmd msg 50 | open f = 51 | Task.perform identity (Task.succeed (f Open)) 52 | 53 | 54 | view : Config msg -> String -> Dialog -> Html msg 55 | view config content opened = 56 | if opened then 57 | let 58 | footer = 59 | case config.strategy of 60 | ConfirmOrClose ( confirmText, confirmMsg ) ( cancelText, cancelMsg ) -> 61 | Html.map config.transform <| 62 | div [ style S.dialogFooter ] 63 | [ button [ style S.defaultButton, onClick (Close cancelMsg) ] [ text cancelText ] 64 | , button [ style S.primaryButton, onClick (Confirm confirmMsg) ] [ text confirmText ] 65 | ] 66 | 67 | clickBackgroundMsg = 68 | config.transform <| 69 | case config.strategy of 70 | ConfirmOrClose _ ( cancelText, cancelMsg ) -> 71 | Close cancelMsg 72 | in 73 | DialogView.viewWithSize clickBackgroundMsg 74 | 100000 75 | 300 76 | 150 77 | [ div [] [ text content ] 78 | , footer 79 | ] 80 | else 81 | text "" 82 | 83 | 84 | 85 | -- 86 | -------------------------------------------------------------------------------- /src/elm/Component/FloorDeleter.elm: -------------------------------------------------------------------------------- 1 | module Component.FloorDeleter exposing (..) 2 | 3 | import Dict 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import InlineHover exposing (hover) 7 | import Util.HtmlUtil as HtmlUtil 8 | import Model.I18n as I18n exposing (Language) 9 | import Model.User as User exposing (User) 10 | import Model.Floor exposing (Floor) 11 | import Component.Dialog as Dialog exposing (Dialog) 12 | import View.Styles as S 13 | 14 | 15 | type Msg 16 | = SelectDeleteFloor 17 | | DialogMsg (Dialog.Msg Msg) 18 | | DeleteFloor Floor Bool 19 | 20 | 21 | type alias Config msg = 22 | { onDeleteFloor : Floor -> Cmd msg 23 | } 24 | 25 | 26 | type alias FloorDeleter = 27 | { dialog : Dialog 28 | } 29 | 30 | 31 | init : FloorDeleter 32 | init = 33 | { dialog = Dialog.init 34 | } 35 | 36 | 37 | update : (Msg -> msg) -> Config msg -> Msg -> FloorDeleter -> ( FloorDeleter, Cmd msg ) 38 | update transform config message model = 39 | case message of 40 | SelectDeleteFloor -> 41 | ( model, Dialog.open DialogMsg |> Cmd.map transform ) 42 | 43 | DialogMsg msg -> 44 | let 45 | ( dialog, cmd ) = 46 | Dialog.update msg model.dialog 47 | in 48 | ( { model 49 | | dialog = dialog 50 | } 51 | , cmd |> Cmd.map transform 52 | ) 53 | 54 | DeleteFloor floor ok -> 55 | ( model 56 | , if ok then 57 | config.onDeleteFloor floor 58 | else 59 | Cmd.none 60 | ) 61 | 62 | 63 | button : Language -> User -> Floor -> Html Msg 64 | button lang user floor = 65 | if User.isAdmin user && Dict.isEmpty floor.objects then 66 | hover S.deleteFloorButtonHover 67 | Html.button 68 | [ HtmlUtil.onClick_ SelectDeleteFloor 69 | , style S.deleteFloorButton 70 | ] 71 | [ text (I18n.deleteFloor lang) 72 | ] 73 | else 74 | text "" 75 | 76 | 77 | dialog : Floor -> FloorDeleter -> Html Msg 78 | dialog floor model = 79 | Dialog.view 80 | { strategy = 81 | Dialog.ConfirmOrClose 82 | ( "delete", DeleteFloor floor True ) 83 | ( "cancel", DeleteFloor floor False ) 84 | , transform = DialogMsg 85 | } 86 | "delete this floor?" 87 | model.dialog 88 | -------------------------------------------------------------------------------- /src/elm/Model/Scale.elm: -------------------------------------------------------------------------------- 1 | module Model.Scale exposing (..) 2 | 3 | import CoreType exposing (..) 4 | 5 | 6 | -- CONFIG 7 | 8 | 9 | minScale : Int 10 | minScale = 11 | 0 12 | 13 | 14 | maxScale : Int 15 | maxScale = 16 | 8 17 | 18 | 19 | defaultScale : Int 20 | defaultScale = 21 | 4 22 | 23 | 24 | step : Float 25 | step = 26 | 1.414 27 | 28 | 29 | 30 | -- TEA 31 | 32 | 33 | type alias Scale = 34 | { scaleDown : Int 35 | } 36 | 37 | 38 | default : Scale 39 | default = 40 | init defaultScale 41 | 42 | 43 | init : Int -> Scale 44 | init scaleDown = 45 | Scale scaleDown 46 | 47 | 48 | type Msg 49 | = ScaleUp 50 | | ScaleDown 51 | 52 | 53 | update : Msg -> Scale -> Scale 54 | update msg scale = 55 | case msg of 56 | ScaleUp -> 57 | { scale | scaleDown = max minScale (scale.scaleDown - 1) } 58 | 59 | ScaleDown -> 60 | { scale | scaleDown = min maxScale (scale.scaleDown + 1) } 61 | 62 | 63 | 64 | -- FUNCTIONS 65 | 66 | 67 | screenToImageForPosition : Scale -> Position -> Position 68 | screenToImageForPosition scale screenPosition = 69 | Position 70 | (screenToImage scale screenPosition.x) 71 | (screenToImage scale screenPosition.y) 72 | 73 | 74 | imageToScreenForPosition : Scale -> Position -> Position 75 | imageToScreenForPosition scale imagePosition = 76 | Position 77 | (imageToScreen scale imagePosition.x) 78 | (imageToScreen scale imagePosition.y) 79 | 80 | 81 | imageToScreenForSize : Scale -> Size -> Size 82 | imageToScreenForSize scale { width, height } = 83 | Size (imageToScreen scale width) (imageToScreen scale height) 84 | 85 | 86 | screenToImageForSize : Scale -> Size -> Size 87 | screenToImageForSize scale { width, height } = 88 | Size (screenToImage scale width) (screenToImage scale height) 89 | 90 | 91 | screenToImage : Scale -> Int -> Int 92 | screenToImage scale imageLength = 93 | round (toFloat imageLength * step ^ toFloat scale.scaleDown) 94 | 95 | 96 | imageToScreen : Scale -> Int -> Int 97 | imageToScreen scale screenLength = 98 | round (toFloat screenLength / step ^ toFloat scale.scaleDown) 99 | 100 | 101 | imageToScreenRatio : Scale -> Float 102 | imageToScreenRatio scale = 103 | 1.0 / step ^ (toFloat scale.scaleDown) 104 | 105 | 106 | ratio : Scale -> Scale -> Float 107 | ratio old new = 108 | (step ^ toFloat old.scaleDown) / (step ^ toFloat new.scaleDown) 109 | -------------------------------------------------------------------------------- /src/elm/Util/UndoList.elm: -------------------------------------------------------------------------------- 1 | module Util.UndoList exposing (UndoList, init, undo, undoReplace, redo, redoReplace, new) 2 | 3 | 4 | type alias UndoList a = 5 | { past : List a 6 | , present : a 7 | , future : List a 8 | } 9 | 10 | 11 | init : a -> UndoList a 12 | init data = 13 | { past = [] 14 | , present = data 15 | , future = [] 16 | } 17 | 18 | 19 | undo : UndoList a -> UndoList a 20 | undo undoList = 21 | case undoList.past of 22 | x :: xs -> 23 | { undoList 24 | | past = xs 25 | , present = x 26 | , future = undoList.present :: undoList.future 27 | } 28 | 29 | _ -> 30 | undoList 31 | 32 | 33 | undoReplace : b -> (a -> a -> ( a, b )) -> UndoList a -> ( UndoList a, b ) 34 | undoReplace default f undoList = 35 | case undoList.past of 36 | x :: xs -> 37 | let 38 | ( newPresent, b ) = 39 | f x undoList.present 40 | in 41 | ( { undoList 42 | | past = xs 43 | , present = newPresent 44 | , future = undoList.present :: undoList.future 45 | } 46 | , b 47 | ) 48 | 49 | _ -> 50 | ( undoList, default ) 51 | 52 | 53 | redo : UndoList a -> UndoList a 54 | redo undoList = 55 | case undoList.future of 56 | x :: xs -> 57 | { undoList 58 | | past = undoList.present :: undoList.past 59 | , present = x 60 | , future = xs 61 | } 62 | 63 | _ -> 64 | undoList 65 | 66 | 67 | redoReplace : b -> (a -> a -> ( a, b )) -> UndoList a -> ( UndoList a, b ) 68 | redoReplace default f undoList = 69 | case undoList.future of 70 | x :: xs -> 71 | let 72 | ( newPresent, b ) = 73 | f x undoList.present 74 | in 75 | ( { undoList 76 | | past = undoList.present :: undoList.past 77 | , present = newPresent 78 | , future = xs 79 | } 80 | , b 81 | ) 82 | 83 | _ -> 84 | ( undoList, default ) 85 | 86 | 87 | new : a -> UndoList a -> UndoList a 88 | new a undoList = 89 | { undoList 90 | | past = undoList.present :: undoList.past 91 | , present = a 92 | , future = [] 93 | } 94 | 95 | 96 | 97 | -- 98 | -------------------------------------------------------------------------------- /src/elm/Page/Map/PrintGuide.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.PrintGuide exposing (view) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Events exposing (..) 6 | import Html.Lazy as Lazy exposing (..) 7 | import Model.I18n as I18n exposing (Language) 8 | import View.Styles as Styles 9 | import Util.StyleUtil exposing (..) 10 | import Page.Map.Msg exposing (Msg(..)) 11 | 12 | 13 | view : Language -> Bool -> Html Msg 14 | view lang isPrintMode = 15 | if isPrintMode then 16 | div 17 | [ style containerStyle 18 | , class "no-print" 19 | ] 20 | [ div [ style (itemStyle 2245 1587) ] [ text "A2" ] 21 | , div [ style (itemStyle 1587 1122) ] [ text "A3" ] 22 | , div [ style (itemStyle 1122 793) ] [ text "A4", lazy button lang ] 23 | ] 24 | else 25 | text "" 26 | 27 | 28 | button : Language -> Html Msg 29 | button lang = 30 | div 31 | [ style buttonStyle 32 | , onClick Print 33 | ] 34 | [ text (I18n.print lang) ] 35 | 36 | 37 | color : String 38 | color = 39 | "rgb(200, 150, 220)" 40 | 41 | 42 | containerStyle : List ( String, String ) 43 | containerStyle = 44 | [ ( "position", "fixed" ) 45 | , ( "z-index", Styles.zPrintGuide ) 46 | , ( "top", "0" ) 47 | , ( "left", "0" ) 48 | , ( "pointer-events", "none" ) 49 | ] 50 | 51 | 52 | itemStyle : Int -> Int -> List ( String, String ) 53 | itemStyle width height = 54 | [ ( "position", "fixed" ) 55 | , ( "top", "0" ) 56 | , ( "left", "0" ) 57 | , ( "width", px width ) 58 | , ( "height", px height ) 59 | , ( "border", "dashed 5px " ++ color ) 60 | , ( "font-size", "x-large" ) 61 | , ( "font-weight", "bold" ) 62 | , ( "color", color ) 63 | , ( "text-align", "right" ) 64 | , ( "padding-right", "3px" ) 65 | ] 66 | 67 | 68 | buttonStyle : List ( String, String ) 69 | buttonStyle = 70 | [ ( "border", "2px solid " ++ color ) 71 | , ( "color", "white" ) 72 | , ( "margin", "auto" ) 73 | , ( "position", "absolute" ) 74 | , ( "top", "0" ) 75 | , ( "bottom", "0" ) 76 | , ( "left", "0" ) 77 | , ( "right", "0" ) 78 | , ( "width", "300px" ) 79 | , ( "height", "150px" ) 80 | , ( "line-height", "150px" ) 81 | , ( "text-align", "center" ) 82 | , ( "font-size", "3em" ) 83 | , ( "font-weight", "normal" ) 84 | , ( "cursor", "pointer" ) 85 | , ( "background-color", color ) 86 | , ( "opacity", "0.4" ) 87 | , ( "pointer-events", "all" ) 88 | ] 89 | -------------------------------------------------------------------------------- /server/lib/account-service.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var log = require('./log.js'); 3 | 4 | function send(token, method, url, data) { 5 | return new Promise((resolve, reject) => { 6 | var options = { 7 | method: method, 8 | url: url, 9 | headers: { 10 | 'Authorization': 'JWT ' + token 11 | }, 12 | body: data, 13 | json: true 14 | }; 15 | request(options, function(e, response, body) { 16 | if (e || response.statusCode >= 400) { 17 | log.system.error(response.statusCode, 'account service: failed ' + method + ' ' + url); 18 | body && log.system.error(body.message); 19 | if (response && response.statusCode === 401) { 20 | reject(401); 21 | } else { 22 | reject(body ? body.message : e || response.statusCode); 23 | } 24 | } else { 25 | log.system.debug(response.statusCode, 'account service: success ' + method + ' ' + url); 26 | resolve(body); 27 | } 28 | }); 29 | }); 30 | } 31 | 32 | function get(token, url) { 33 | return send(token, 'GET', url); 34 | } 35 | 36 | function post(token, url, data) { 37 | return send(token, 'POST', url, data); 38 | } 39 | 40 | function login(root, userId, password) { 41 | return post('', root + '/1/authentication', { 42 | userId: userId, 43 | password: password 44 | }).then(obj => { 45 | return obj.accessToken; 46 | }); 47 | } 48 | 49 | function addUser(root, token, user) { 50 | user.userId = user.id; 51 | user.password = user.pass; 52 | user.role = user.role.toUpperCase(); 53 | return post(token, root + '/1/users', user); 54 | } 55 | 56 | function getAllAdmins(root, token, exclusiveStartKey) { 57 | var url = root + '/1/users' + 58 | (exclusiveStartKey ? '?exclusiveStartKey=' + exclusiveStartKey : '') 59 | return get(token, url).then((data) => { 60 | var users = data.users.map(user => { 61 | toMapUser(user); 62 | return user; 63 | }).filter(user => { 64 | return user.role === 'admin'; 65 | }); 66 | if (data.lastEvaluatedKey) { 67 | return getAllAdmins(root, token, data.lastEvaluatedKey).then((users2) => { 68 | return Promise.resolve(users.concat(users2)); 69 | }); 70 | } else { 71 | return Promise.resolve(users); 72 | } 73 | }); 74 | } 75 | 76 | function toMapUser(user) { 77 | user.id = user.id || user.userId; 78 | user.role = user.role.toLowerCase(); 79 | user.tenantId = ''; 80 | } 81 | 82 | module.exports = { 83 | addUser: addUser, 84 | login: login, 85 | getAllAdmins: getAllAdmins, 86 | toMapUser: toMapUser 87 | }; 88 | -------------------------------------------------------------------------------- /src/elm/Page/Map/URL.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.URL exposing (..) 2 | 3 | import Dict 4 | import Navigation exposing (Location) 5 | import Util.UrlParser as UrlParser 6 | 7 | 8 | type alias URL = 9 | { floorId : Maybe String 10 | , query : Maybe String 11 | , objectId : Maybe String 12 | , editMode : Bool 13 | } 14 | 15 | 16 | parse : Location -> Result String URL 17 | parse location = 18 | let 19 | floorId = 20 | getFloorId location 21 | 22 | dict = 23 | UrlParser.parseSearch location.search 24 | in 25 | Result.map 26 | (\floorId -> 27 | URL 28 | floorId 29 | (Dict.get "q" dict) 30 | (Dict.get "object" dict) 31 | (Dict.member "edit" dict) 32 | ) 33 | floorId 34 | 35 | 36 | getFloorId : Location -> Result String (Maybe String) 37 | getFloorId location = 38 | if String.startsWith "#" location.hash then 39 | let 40 | id = 41 | String.dropLeft 1 location.hash 42 | in 43 | if String.length id == 36 then 44 | Ok (Just id) 45 | else if String.length id == 0 then 46 | Ok Nothing 47 | else 48 | Err ("invalid floorId: " ++ id) 49 | else 50 | Ok Nothing 51 | 52 | 53 | init : URL 54 | init = 55 | { floorId = Nothing 56 | , query = Nothing 57 | , objectId = Nothing 58 | , editMode = False 59 | } 60 | 61 | 62 | stringify : String -> URL -> String 63 | stringify root { floorId, query, objectId, editMode } = 64 | let 65 | params = 66 | (List.filterMap 67 | (\( key, maybeValue ) -> Maybe.map (\v -> ( key, v )) maybeValue) 68 | [ ( "q", query ) 69 | , ( "object", objectId ) 70 | ] 71 | ) 72 | ++ (if editMode then 73 | [ ( "edit", "true" ) ] 74 | else 75 | [] 76 | ) 77 | in 78 | case floorId of 79 | Just id -> 80 | root ++ stringifyParams params ++ "#" ++ id 81 | 82 | Nothing -> 83 | root ++ stringifyParams params 84 | 85 | 86 | stringifyParams : List ( String, String ) -> String 87 | stringifyParams params = 88 | if params == [] then 89 | "" 90 | else 91 | "?" 92 | ++ (String.join "&" <| 93 | List.map (\( k, v ) -> k ++ "=" ++ v) params 94 | ) 95 | 96 | 97 | 98 | -- 99 | -------------------------------------------------------------------------------- /src/elm/Page/Map/PrototypePreviewView.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.PrototypePreviewView exposing (view) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Util.HtmlUtil exposing (..) 6 | import Util.ListUtil exposing (..) 7 | import Model.Prototype exposing (Prototype) 8 | import Model.Prototypes as Prototypes exposing (PositionedPrototype) 9 | import View.Styles as S 10 | import View.PrototypePreviewView as PrototypePreviewView 11 | import Page.Map.Msg exposing (..) 12 | import CoreType exposing (..) 13 | 14 | 15 | containerSize : Size 16 | containerSize = 17 | Size (320 - (20 * 2)) 180 18 | 19 | 20 | 21 | -- TODO 22 | 23 | 24 | view : List ( Prototype, Bool ) -> Html Msg 25 | view prototypes = 26 | let 27 | selectedIndex = 28 | zipWithIndex prototypes 29 | |> List.filterMap 30 | (\( ( prototype, selected ), index ) -> 31 | if selected then 32 | Just index 33 | else 34 | Nothing 35 | ) 36 | |> List.head 37 | |> Maybe.withDefault 0 38 | 39 | buttonsView = 40 | buttons (List.length prototypes) selectedIndex 41 | 42 | box = 43 | PrototypePreviewView.view 44 | containerSize 45 | selectedIndex 46 | (List.map Tuple.first prototypes) 47 | in 48 | div 49 | [ style [ ( "position", "relative" ) ] ] 50 | (box :: buttonsView) 51 | 52 | 53 | buttons : Int -> Int -> List (Html Msg) 54 | buttons prototypeLength selectedIndex = 55 | List.map 56 | (\isLeft -> 57 | let 58 | label = 59 | if isLeft then 60 | "<" 61 | else 62 | ">" 63 | in 64 | div 65 | [ style (S.prototypePreviewScroll isLeft) 66 | , onClick_ 67 | (if isLeft then 68 | PrototypesMsg Prototypes.prev 69 | else 70 | PrototypesMsg Prototypes.next 71 | ) 72 | ] 73 | [ text label ] 74 | ) 75 | ((if selectedIndex > 0 then 76 | [ True ] 77 | else 78 | [] 79 | ) 80 | ++ (if selectedIndex < prototypeLength - 1 then 81 | [ False ] 82 | else 83 | [] 84 | ) 85 | ) 86 | -------------------------------------------------------------------------------- /persistent-cache/src/Dag.elm: -------------------------------------------------------------------------------- 1 | module Dag 2 | exposing 3 | ( Dag 4 | , Edge 5 | , fromList 6 | , shortestPath 7 | ) 8 | 9 | import Dict exposing (Dict) 10 | 11 | 12 | type alias Edge a = 13 | { from : Int 14 | , to : Int 15 | , value : a 16 | } 17 | 18 | 19 | type alias Dag a = 20 | Dict Int (List ( Int, a )) 21 | 22 | 23 | 24 | -- CREATE 25 | 26 | 27 | fromList : List (Edge a) -> Dag a 28 | fromList edges = 29 | List.foldl addNode Dict.empty edges 30 | 31 | 32 | addNode : Edge a -> Dag a -> Dag a 33 | addNode { from, to, value } graph = 34 | let 35 | addNodeHelp maybeNode = 36 | case maybeNode of 37 | Nothing -> 38 | Just [ ( to, value ) ] 39 | 40 | Just kids -> 41 | Just (( to, value ) :: kids) 42 | in 43 | if from < to then 44 | Dict.update from addNodeHelp graph 45 | else 46 | graph 47 | 48 | 49 | 50 | -- SHORTEST PATH 51 | 52 | 53 | shortestPath : Int -> Int -> Dag a -> Maybe (List a) 54 | shortestPath root goal graph = 55 | Maybe.map .path (shortestPathHelp root goal graph) 56 | 57 | 58 | type alias Path a = 59 | { length : Int 60 | , path : List a 61 | } 62 | 63 | 64 | shortestPathHelp : Int -> Int -> Dag a -> Maybe (Path a) 65 | shortestPathHelp root goal graph = 66 | if root == goal then 67 | Just (Path 0 []) 68 | else 69 | case Dict.get root graph of 70 | Nothing -> 71 | Nothing 72 | 73 | Just kids -> 74 | kids 75 | |> List.filterMap (shartestSubPath goal graph) 76 | |> pickShortest Nothing 77 | 78 | 79 | shartestSubPath : Int -> Dag a -> ( Int, a ) -> Maybe (Path a) 80 | shartestSubPath goal graph ( root, value ) = 81 | case shortestPathHelp root goal graph of 82 | Nothing -> 83 | Nothing 84 | 85 | Just { length, path } -> 86 | Just (Path (length + 1) (value :: path)) 87 | 88 | 89 | pickShortest : Maybe (Path a) -> List (Path a) -> Maybe (Path a) 90 | pickShortest shortest unexplored = 91 | case unexplored of 92 | [] -> 93 | shortest 94 | 95 | contender :: rest -> 96 | case shortest of 97 | Nothing -> 98 | Just contender 99 | 100 | Just { length } -> 101 | if contender.length < length then 102 | Just contender 103 | else 104 | shortest 105 | -------------------------------------------------------------------------------- /src/elm/Model/ProfilePopupLogic.elm: -------------------------------------------------------------------------------- 1 | module Model.ProfilePopupLogic exposing (..) 2 | 3 | import Model.Scale as Scale exposing (Scale) 4 | import Model.Object as Object exposing (Object) 5 | import CoreType exposing (..) 6 | 7 | 8 | personPopupSize : Size 9 | personPopupSize = 10 | Size 300 180 11 | 12 | 13 | centerTopScreenXYOfObject : Scale -> Position -> Object -> Position 14 | centerTopScreenXYOfObject scale offset object = 15 | let 16 | { x, y } = 17 | Object.positionOf object 18 | 19 | { width } = 20 | Object.sizeOf object 21 | in 22 | Scale.imageToScreenForPosition 23 | scale 24 | (Position (offset.x + x + width // 2) (offset.y + y)) 25 | 26 | 27 | bottomScreenYOfObject : Scale -> Position -> Object -> Int 28 | bottomScreenYOfObject scale offset object = 29 | let 30 | { x, y } = 31 | Object.positionOf object 32 | 33 | { height } = 34 | Object.sizeOf object 35 | in 36 | Scale.imageToScreen scale (offset.y + y + height) 37 | 38 | 39 | calcPopupLeftFromObjectCenter : Int -> Int -> Int 40 | calcPopupLeftFromObjectCenter popupWidth objCenter = 41 | objCenter - (popupWidth // 2) 42 | 43 | 44 | calcPopupRightFromObjectCenter : Int -> Int -> Int 45 | calcPopupRightFromObjectCenter popupWidth objCenter = 46 | objCenter + (popupWidth // 2) 47 | 48 | 49 | calcPopupTopFromObjectTop : Int -> Int -> Int 50 | calcPopupTopFromObjectTop popupHeight objTop = 51 | objTop - (popupHeight + 10) 52 | 53 | 54 | adjustOffset : Size -> Size -> Scale -> Position -> Object -> Position 55 | adjustOffset containerSize popupSize scale offset object = 56 | let 57 | objCenterTop = 58 | centerTopScreenXYOfObject scale offset object 59 | 60 | left = 61 | calcPopupLeftFromObjectCenter popupSize.width objCenterTop.x 62 | 63 | top = 64 | calcPopupTopFromObjectTop popupSize.height objCenterTop.y 65 | 66 | right = 67 | calcPopupRightFromObjectCenter popupSize.width objCenterTop.x 68 | 69 | bottom = 70 | bottomScreenYOfObject scale offset object 71 | 72 | offsetX_ = 73 | adjust scale containerSize.width left right offset.x 74 | 75 | offsetY_ = 76 | adjust scale containerSize.height top bottom offset.y 77 | in 78 | Position offsetX_ offsetY_ 79 | 80 | 81 | adjust : Scale -> Int -> Int -> Int -> Int -> Int 82 | adjust scale length min max offset = 83 | if min < 0 then 84 | offset - Scale.screenToImage scale (min - 0) 85 | else if max > length then 86 | offset - Scale.screenToImage scale (max - length) 87 | else 88 | offset 89 | -------------------------------------------------------------------------------- /Spec.md: -------------------------------------------------------------------------------- 1 | Spec 2 | ==== 3 | 4 | ## Routing 5 | 6 | |URL| 7 | |:--| 8 | |/| 9 | |/login| 10 | |/#floorId| 11 | |/?q=foo&edit=#floorId| 12 | 13 | 14 | ## REST API 15 | 16 | (TODO: write by swagger) 17 | 18 | Currently, all of these API are used by client-side program. No public API for other services. 19 | 20 | |Method|URL|Req Body|Res Body|Description|Guest|General|Admin| 21 | |:--|:--|:--|:--|:--|:--|:--|:--| 22 | |GET| /api/1/search/:query||[SearchResult]||✓|✓|✓| 23 | |GET| /api/1/auth||User||✓|✓|✓| 24 | |GET| /api/1/people||[Person]||✓|✓|✓| 25 | |GET| /api/1/people/:personId||Person||✓|✓|✓| 26 | |GET| /api/1/candidates/:name||[Person]||✓|✓|✓| 27 | |GET| /api/1/colors||[Color]||✓|✓|✓| 28 | |GET| /api/1/prototypes||[Prototype]||✓|✓|✓| 29 | |PUT| /api/1/prototypes/:id|Prototype|||||✓| 30 | |DELETE| /api/1/prototypes/:id||||||✓| 31 | |GET| /api/1/floors||[FloorInfo]||✓|✓|✓| 32 | |GET| /api/1/floors?all=true||[FloorInfo]||✓|✓|✓| 33 | |GET| /api/1/floors/:id||Floor|fetch latest version|✓|✓|✓| 34 | |GET| /api/1/floors/:id?all=true||Floor|fetch latest unpublished version||✓|✓| 35 | |PUT| /api/1/floors/:id|FloorChange|Floor|update latest unpublished version||✓|✓| 36 | |PUT| /api/1/floors/:id/public||Floor|publish latest unpublished version|||✓| 37 | |PUT| /api/1/images/:id|Image|||||✓| 38 | 39 | 46 | 47 | ### Types 48 | 49 | |Type|Structure| 50 | |:--|:--| 51 | |User| { id : UUID, name : Person.name, role : Role, personId : String } | 52 | |Floor| { id : UUID, ord : Int, version : Int, name : String, image? : URL, realSize? : (Int, Int), objects : [ Object ], public : Bool, publishedBy? : User.id, publishedAt? : Date } | 53 | |FloorChange| { id : UUID, ord : Int, version : Int, name : String, image? : URL, realSize? : (Int, Int), add : [ Object ], modify : [ (Object, Object) ], delete : [ Object ], public : Bool, publishedBy? : User.id, publishedAt? : Date } | 54 | |Object| { id : UUID, type: String, name : String, size : (Int, Int), color : String, fontSize : Float, shape : String, personId? : String } | 55 | |Prototype| { id : UUID, name : String, size : (Int, Int), color : String } | 56 | |Image| Binary | 57 | |SearchResult| [(Object, String)] | 58 | |Person| { id : String, name : String, post : String, tel? : String, mail? : String, image? : URL } | 59 | |Role| "admin" "general" | 60 | |Color| { id : String, ord : Int, type : String, color : String } | 61 | |UUID| String | 62 | |Date| Int | 63 | |URL| String | 64 | 65 | 66 | ### Table Definition 67 | 68 | See [here](server/sql/2-create-tables.sql). 69 | -------------------------------------------------------------------------------- /src/elm/Model/DateFormatter.elm: -------------------------------------------------------------------------------- 1 | module Model.DateFormatter exposing (formatDate, formatDateOrTime) 2 | 3 | import Date exposing (..) 4 | import Model.I18n exposing (Language(..)) 5 | 6 | 7 | monthToInt : Month -> Int 8 | monthToInt month = 9 | case month of 10 | Jan -> 11 | 1 12 | 13 | Feb -> 14 | 2 15 | 16 | Mar -> 17 | 3 18 | 19 | Apr -> 20 | 4 21 | 22 | May -> 23 | 5 24 | 25 | Jun -> 26 | 6 27 | 28 | Jul -> 29 | 7 30 | 31 | Aug -> 32 | 8 33 | 34 | Sep -> 35 | 9 36 | 37 | Oct -> 38 | 10 39 | 40 | Nov -> 41 | 11 42 | 43 | Dec -> 44 | 12 45 | 46 | 47 | sameDay : Date -> Date -> Bool 48 | sameDay d1 d2 = 49 | year d1 50 | == year d2 51 | && month d1 52 | == month d2 53 | && day d1 54 | == day d2 55 | 56 | 57 | am : Date -> Bool 58 | am date = 59 | if hour date < 12 then 60 | True 61 | else 62 | False 63 | 64 | 65 | pm : Date -> Bool 66 | pm = 67 | not << am 68 | 69 | 70 | hourOfAmPm : Int -> Int 71 | hourOfAmPm hour = 72 | if hour > 12 then 73 | hour - 12 74 | else 75 | hour 76 | 77 | 78 | ampm : Date -> String 79 | ampm date = 80 | toString (hourOfAmPm (hour date)) 81 | ++ ":" 82 | ++ fillZero2 (toString (minute date)) 83 | ++ " " 84 | ++ (if am date then 85 | "a.m." 86 | else 87 | "p.m." 88 | ) 89 | 90 | 91 | formatTime : Language -> Date -> String 92 | formatTime lang date = 93 | case lang of 94 | JA -> 95 | fillZero2 (toString (hour date)) 96 | ++ ":" 97 | ++ fillZero2 (toString (minute date)) 98 | 99 | EN -> 100 | ampm date 101 | 102 | 103 | fillZero2 : String -> String 104 | fillZero2 s = 105 | String.right 2 ("0" ++ s) 106 | 107 | 108 | formatDate : Language -> Date -> String 109 | formatDate lang date = 110 | case lang of 111 | JA -> 112 | toString (Date.year date) 113 | ++ "/" 114 | ++ toString (monthToInt <| Date.month date) 115 | ++ "/" 116 | ++ toString (Date.day date) 117 | 118 | EN -> 119 | toString (monthToInt <| Date.month date) 120 | ++ "/" 121 | ++ toString (Date.day date) 122 | ++ "/" 123 | ++ toString (Date.year date) 124 | 125 | 126 | formatDateOrTime : Language -> Date -> Date -> String 127 | formatDateOrTime lang now date = 128 | if sameDay now date then 129 | formatTime lang date 130 | else 131 | formatDate lang date 132 | -------------------------------------------------------------------------------- /src/elm/API/Cache.elm: -------------------------------------------------------------------------------- 1 | module API.Cache exposing (..) 2 | 3 | import Task exposing (..) 4 | import Json.Encode as E exposing (Value) 5 | import Json.Decode as D exposing (Decoder) 6 | import PersistentCache as Cache 7 | import Model.Scale as Scale exposing (Scale) 8 | import Model.I18n as I18n exposing (..) 9 | import Util.DecodeUtil exposing (..) 10 | 11 | 12 | type alias Cache = 13 | Cache.Cache UserState 14 | 15 | 16 | cache : Cache 17 | cache = 18 | Cache.cache 19 | { name = "userState" 20 | , version = 1 21 | , kilobytes = 1024 22 | , decode = decode 23 | , encode = encode 24 | } 25 | 26 | 27 | defaultUserState : Language -> UserState 28 | defaultUserState lang = 29 | { scale = Scale.default 30 | , offset = { x = 35, y = 35 } 31 | , lang = lang 32 | } 33 | 34 | 35 | get : Cache -> Task x (Maybe UserState) 36 | get cache = 37 | Cache.get cache "userState" 38 | 39 | 40 | getWithDefault : Cache -> UserState -> Task x UserState 41 | getWithDefault cache defaultState = 42 | Cache.get cache "userState" 43 | |> andThen 44 | (\maybeState -> 45 | case maybeState of 46 | Just state -> 47 | Task.succeed state 48 | 49 | Nothing -> 50 | put cache defaultState 51 | |> Task.map (\_ -> defaultState) 52 | ) 53 | 54 | 55 | put : Cache -> UserState -> Task x () 56 | put cache state = 57 | Cache.add cache "userState" state 58 | 59 | 60 | clear : Cache -> Task x () 61 | clear cache = 62 | Cache.clear cache 63 | 64 | 65 | type alias Position = 66 | { x : Int 67 | , y : Int 68 | } 69 | 70 | 71 | type alias UserState = 72 | { scale : Scale 73 | , offset : Position 74 | , lang : Language 75 | } 76 | 77 | 78 | decode : Decoder UserState 79 | decode = 80 | D.map3 81 | (\scale ( x, y ) lang -> 82 | { scale = Scale.init scale 83 | , offset = { x = x, y = y } 84 | , lang = 85 | if lang == "JA" then 86 | I18n.JA 87 | else 88 | I18n.EN 89 | } 90 | ) 91 | (D.field "scale" D.int) 92 | (D.field "offset" <| tuple2 (,) D.int D.int) 93 | (D.field "lang" D.string) 94 | 95 | 96 | encode : UserState -> Value 97 | encode state = 98 | E.object 99 | [ ( "scale", E.int state.scale.scaleDown ) 100 | , ( "offset", E.list [ E.int state.offset.x, E.int state.offset.y ] ) 101 | , ( "lang" 102 | , E.string <| 103 | case state.lang of 104 | JA -> 105 | "JA" 106 | 107 | EN -> 108 | "EN" 109 | ) 110 | ] 111 | -------------------------------------------------------------------------------- /server/lib/search-optimizer.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/kawanet/5553478 2 | 3 | function normalizeKana(src) { 4 | return src.replace(/[\u30a1-\u30f6]/g, function(match) { 5 | var chr = match.charCodeAt(0) - 0x60; 6 | return String.fromCharCode(chr); 7 | }); 8 | } 9 | 10 | // http://www13.plala.or.jp/bigdata/kanji_2.html 11 | function normalizeKanji(src) { 12 | return src 13 | .replace(/[髙]/g, '高') 14 | .replace(/[槗𣘺𣘺嵜]/g, '橋') 15 | .replace(/[斎齊齋]/g, '斉') 16 | .replace(/[籐]/g, '藤') 17 | .replace(/[邊邉邉󠄂邉󠄃邉󠄄邉󠄅邉󠄆邉󠄇邉󠄈邉󠄉邉󠄊邉󠄋邉󠄌邉󠄍邉󠄎邊󠄁邊󠄂邊󠄃邊󠄄邊󠄅邊󠄆邊󠄇]/g, '辺') 18 | .replace(/[𠮷]/g, '吉') 19 | .replace(/[濱濵]/g, '浜') 20 | .replace(/[﨑碕嵜]/g, '崎'); 21 | } 22 | 23 | function normalizeRoma(src) { 24 | return src 25 | .replace(/shi/g, 'si') 26 | .replace(/chi/g, 'ti') 27 | .replace(/tsu/g, 'tu') 28 | .replace(/sya/g, 'sha') 29 | .replace(/syu/g, 'shu') 30 | .replace(/syo/g, 'sho') 31 | .replace(/tya/g, 'cha') 32 | .replace(/tyu/g, 'chu') 33 | .replace(/tyo/g, 'cho'); 34 | } 35 | 36 | // http://www.asahi-net.or.jp/~ax2s-kmtn/ref/unicode/u2460.html 37 | function normalizeNumber(src) { 38 | return src 39 | .replace(/[\u2460-\u2473]/g, function(match) { 40 | return (match.charCodeAt(0) - 0x2460 + 1) + ''; 41 | }) 42 | .replace(/[\u2474-\u2487]/g, function(match) { 43 | return (match.charCodeAt(0) - 0x2474 + 1) + ''; 44 | }) 45 | .replace(/[\u2488-\u249b]/g, function(match) { 46 | return (match.charCodeAt(0) - 0x2488 + 1) + ''; 47 | }); 48 | } 49 | 50 | // http://www.asahi-net.or.jp/~ax2s-kmtn/ref/unicode/uff00.html 51 | function normalizeFullWidth(src) { 52 | return src.replace(/[\uff01-\uff5e]/g, function(match) { 53 | var chr = match.charCodeAt(0) - 0xff01 + 0x21; 54 | return String.fromCharCode(chr); 55 | }); 56 | } 57 | 58 | // https://github.com/jinjor/ja-en-separator/blob/master/package/content-script.js 59 | function separateJaEn(src) { 60 | return src 61 | .replace(/ ?([^\x01-\x7E]+) ?/g, ' $1 ') 62 | .replace(/^ /, '') 63 | .replace(/ $/, ''); 64 | } 65 | 66 | function normalizeSpace(src) { 67 | return src 68 | .replace(/[ \r\n\t]+/g, ' ') 69 | .trim() 70 | .replace(/[ ]+/g, ' '); 71 | } 72 | 73 | function normalize(src) { 74 | var tmp = src; 75 | tmp = normalizeFullWidth(tmp); 76 | tmp = normalizeNumber(tmp); 77 | tmp = tmp.toLowerCase(); 78 | tmp = normalizeKana(tmp); 79 | tmp = normalizeKanji(tmp); 80 | tmp = normalizeRoma(tmp); 81 | tmp = separateJaEn(tmp); 82 | tmp = normalizeSpace(tmp); 83 | return tmp; 84 | } 85 | 86 | function tokenize(src) { 87 | return src.split(/[ \//・]+/g); 88 | } 89 | 90 | function getKeys(object) { 91 | var normalized = normalize(object.name); 92 | var tokens = tokenize(normalized); 93 | return tokens; 94 | } 95 | 96 | module.exports = { 97 | normalize: normalize, 98 | getKeys: getKeys 99 | }; 100 | -------------------------------------------------------------------------------- /src/elm/Page/Map/ClipboardOptionsView.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.ClipboardOptionsView exposing (Form, init, view) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes as Attributes exposing (..) 5 | import Html.Events exposing (..) 6 | import CoreType exposing (..) 7 | import View.Styles as Styles 8 | 9 | 10 | type alias Form = 11 | { width : Maybe String 12 | , height : Maybe String 13 | } 14 | 15 | 16 | init : Form 17 | init = 18 | Form Nothing Nothing 19 | 20 | 21 | maybeError : Result e a -> Maybe e 22 | maybeError result = 23 | case result of 24 | Ok _ -> 25 | Nothing 26 | 27 | Err message -> 28 | Just message 29 | 30 | 31 | type Msg 32 | = ChangeWidth (Result String Int) 33 | | ChangeHeight (Result String Int) 34 | 35 | 36 | update : Size -> Form -> Msg -> ( Form, Maybe Size ) 37 | update size form msg = 38 | case msg of 39 | ChangeWidth result -> 40 | case result of 41 | Ok width -> 42 | ( { form | width = Nothing }, Just { size | width = width } ) 43 | 44 | Err message -> 45 | ( { form | width = Just message }, Nothing ) 46 | 47 | ChangeHeight result -> 48 | case result of 49 | Ok height -> 50 | ( { form | height = Nothing }, Just { size | height = height } ) 51 | 52 | Err message -> 53 | ( { form | height = Just message }, Nothing ) 54 | 55 | 56 | view : (( Form, Maybe Size ) -> msg) -> Form -> Size -> Html msg 57 | view tagger form size = 58 | let 59 | widthLabel = 60 | label [ style Styles.widthHeightLabel ] [ text "幅" ] 61 | 62 | heightLabel = 63 | label [ style Styles.widthHeightLabel ] [ text "高さ" ] 64 | in 65 | div [] 66 | [ div [ style [ ( "font-size", "13px" ) ] ] [ text "スプレッドシート貼付オプション" ] 67 | , div 68 | [ style Styles.floorSizeInputContainer ] 69 | [ widthLabel 70 | , sizeInput size.width |> Html.map ChangeWidth 71 | , heightLabel 72 | , sizeInput size.height |> Html.map ChangeHeight 73 | ] 74 | |> Html.map (update size form >> tagger) 75 | ] 76 | 77 | 78 | sizeInput : Int -> Html (Result String Int) 79 | sizeInput intValue = 80 | input 81 | [ style Styles.realSizeInput 82 | , value (toString intValue) 83 | , onInput validate 84 | ] 85 | [] 86 | 87 | 88 | validate : String -> Result String Int 89 | validate value = 90 | value 91 | |> String.toInt 92 | |> Result.mapError (always "only numbers are allowed") 93 | |> Result.andThen validateInt 94 | 95 | 96 | validateInt : Int -> Result String Int 97 | validateInt i = 98 | if i > 0 then 99 | Ok i 100 | else 101 | Err "only positive numbers are allowed" 102 | -------------------------------------------------------------------------------- /src/elm/Model/Mode.elm: -------------------------------------------------------------------------------- 1 | module Model.Mode exposing (..) 2 | 3 | 4 | type Mode 5 | = Mode 6 | { printMode : Bool 7 | , searchResult : Bool 8 | , editing : Bool 9 | , editMode : EditingMode 10 | } 11 | 12 | 13 | type EditingMode 14 | = Select 15 | | Pen 16 | | Stamp 17 | | Label 18 | 19 | 20 | init : Bool -> Mode 21 | init isEditMode = 22 | Mode 23 | { printMode = False 24 | , searchResult = False 25 | , editing = isEditMode 26 | , editMode = Select 27 | } 28 | 29 | 30 | showingSearchResult : Mode -> Bool 31 | showingSearchResult (Mode mode) = 32 | mode.searchResult 33 | 34 | 35 | toggleEditing : Mode -> Mode 36 | toggleEditing (Mode mode) = 37 | Mode 38 | { mode 39 | | editing = not (mode.editing) 40 | } 41 | 42 | 43 | togglePrintView : Mode -> Mode 44 | togglePrintView (Mode mode) = 45 | Mode 46 | { mode 47 | | printMode = not (mode.printMode) 48 | } 49 | 50 | 51 | toSelectMode : Mode -> Mode 52 | toSelectMode (Mode mode) = 53 | Mode 54 | { mode 55 | | editing = True 56 | , editMode = Select 57 | } 58 | 59 | 60 | toStampMode : Mode -> Mode 61 | toStampMode (Mode mode) = 62 | Mode 63 | { mode 64 | | editing = True 65 | , editMode = Stamp 66 | } 67 | 68 | 69 | isEditMode : Mode -> Bool 70 | isEditMode ((Mode mode) as mode_) = 71 | mode.editing && not (isPrintMode mode_) 72 | 73 | 74 | currentEditMode : Mode -> Maybe EditingMode 75 | currentEditMode (Mode mode) = 76 | if mode.editing then 77 | Just mode.editMode 78 | else 79 | Nothing 80 | 81 | 82 | isSelectMode : Mode -> Bool 83 | isSelectMode (Mode mode) = 84 | mode.editing && mode.editMode == Select 85 | 86 | 87 | isPenMode : Mode -> Bool 88 | isPenMode (Mode mode) = 89 | mode.editing && mode.editMode == Pen 90 | 91 | 92 | isStampMode : Mode -> Bool 93 | isStampMode (Mode mode) = 94 | mode.editing && mode.editMode == Stamp 95 | 96 | 97 | isLabelMode : Mode -> Bool 98 | isLabelMode (Mode mode) = 99 | mode.editing && mode.editMode == Label 100 | 101 | 102 | isViewMode : Mode -> Bool 103 | isViewMode (Mode mode) = 104 | not mode.editing 105 | 106 | 107 | isPrintMode : Mode -> Bool 108 | isPrintMode (Mode mode) = 109 | mode.printMode 110 | 111 | 112 | changeEditingMode : EditingMode -> Mode -> Mode 113 | changeEditingMode editingMode (Mode mode) = 114 | Mode 115 | { mode 116 | | editing = True 117 | , editMode = editingMode 118 | } 119 | 120 | 121 | showSearchResult : Mode -> Mode 122 | showSearchResult (Mode mode) = 123 | Mode 124 | { mode 125 | | searchResult = True 126 | } 127 | 128 | 129 | hideSearchResult : Mode -> Mode 130 | hideSearchResult (Mode mode) = 131 | Mode 132 | { mode 133 | | searchResult = False 134 | } 135 | -------------------------------------------------------------------------------- /server/lib/schema.js: -------------------------------------------------------------------------------- 1 | function floorKeyValues(tenantId, floor, updateAt) { 2 | return [ 3 | ["id", floor.id], 4 | ["version", floor.version], 5 | ["tenantId", tenantId], 6 | ["name", floor.name], 7 | ["ord", floor.ord], 8 | ["image", floor.image], 9 | ["flipImage", floor.flipImage ? 1 : 0], 10 | ["width", floor.width], 11 | ["height", floor.height], 12 | ["realWidth", floor.realWidth], 13 | ["realHeight", floor.realHeight], 14 | ["temporary", floor.temporary ? 1 : 0], 15 | ["updateBy", floor.updateBy], 16 | ["updateAt", updateAt] 17 | ]; 18 | } 19 | 20 | function prototypeKeyValues(tenantId, proto) { 21 | return [ 22 | ["id", proto.id], 23 | ["tenantId", tenantId], 24 | ["name", proto.name], 25 | ["width", proto.width], 26 | ["height", proto.height], 27 | ["backgroundColor", proto.backgroundColor], 28 | ["color", proto.color], 29 | ["fontSize", proto.fontSize], 30 | ["shape", proto.shape] 31 | ]; 32 | } 33 | 34 | function objectKeyValues(object, updateAt) { 35 | return [ 36 | ["id", object.id], 37 | ["name", object.name], 38 | ["type", object.type], 39 | ["x", object.x], 40 | ["y", object.y], 41 | ["width", object.width], 42 | ["height", object.height], 43 | ["backgroundColor", object.backgroundColor], 44 | ["fontSize", object.fontSize], 45 | ["color", object.color], 46 | ["bold", object.bold ? 1 : 0], 47 | ["url", object.url], 48 | ["shape", object.shape], 49 | ["personId", object.personId], 50 | ["floorId", object.floorId], 51 | ["floorVersion", object.floorVersion], 52 | ["updateAt", updateAt] 53 | ]; 54 | } 55 | 56 | function objectOptKeyValues(object) { 57 | return [ 58 | ["id", object.id], 59 | ["name", object.name], 60 | ["type", object.type], 61 | ["x", object.x], 62 | ["y", object.y], 63 | ["width", object.width], 64 | ["height", object.height], 65 | ["backgroundColor", object.backgroundColor], 66 | ["fontSize", object.fontSize], 67 | ["color", object.color], 68 | ["bold", object.bold ? 1 : 0], 69 | ["url", object.url], 70 | ["shape", object.shape], 71 | ["personId", object.personId], 72 | ["personName", object.personName || ''], 73 | ["personEmpNo", object.personEmpNo || ''], 74 | ["personPost", object.personPost || ''], 75 | ["personTel1", object.personTel1 || ''], 76 | ["personTel2", object.personTel2 || ''], 77 | ["personMail", object.personMail || ''], 78 | ["personImage", object.personImage || ''], 79 | ["floorId", object.floorId], 80 | ["editing", object.editing ? 1 : 0], 81 | ["updateAt", object.updateAt] 82 | ]; 83 | } 84 | 85 | 86 | function colorKeyValues(tenantId, c) { 87 | return [ 88 | ["id", c.id], 89 | ["tenantId", tenantId], 90 | ["ord", c.ord], 91 | ["type", c.type], 92 | ["color", c.color] 93 | ]; 94 | } 95 | 96 | module.exports = { 97 | floorKeyValues: floorKeyValues, 98 | prototypeKeyValues: prototypeKeyValues, 99 | objectKeyValues: objectKeyValues, 100 | objectOptKeyValues: objectOptKeyValues, 101 | colorKeyValues: colorKeyValues 102 | }; 103 | -------------------------------------------------------------------------------- /server/commands.js: -------------------------------------------------------------------------------- 1 | var db = require('./lib/db.js'); 2 | var rdb = require('./lib/mysql.js'); 3 | var mock = require('./lib/mock.js'); 4 | var config = require('./lib/config.js'); 5 | var accountService = require('./lib/account-service.js'); 6 | 7 | var rdbEnv = rdb.createEnv(config.mysql.host, config.mysql.user, config.mysql.pass, 'map2'); 8 | 9 | function login() { 10 | if (!config.operationUser && !config.operationPass) { 11 | // server don't need token. 12 | return Promise.resolve(null); 13 | } else { 14 | // debugging user may need token. 15 | return accountService.login(config.accountServiceRoot, config.operationUser, config.operationPass); 16 | } 17 | } 18 | 19 | var commands = {}; 20 | 21 | commands.createObjectOptTable = function() { 22 | return login().then(token => { 23 | return rdbEnv.forConnectionAndTransaction(conn => { 24 | console.log(`creating optimized object table ...`); 25 | return db.createObjectOptTable(conn, config.profileServiceRoot, token, true).then(_ => { 26 | return db.createObjectOptTable(conn, config.profileServiceRoot, token, false); 27 | }); 28 | }); 29 | }); 30 | } 31 | 32 | commands.createInitialData = function(tenantId) { 33 | tenantId = tenantId || ''; 34 | if (config.multiTenency && !tenantId) { 35 | return Promise.reject('tenantId is not defined.'); 36 | } 37 | return rdbEnv.forConnectionAndTransaction(conn => { 38 | console.log(`creating data for tenant ${tenantId} ...`); 39 | return db.savePrototypes(conn, tenantId, mock.prototypes).then(() => { 40 | return db.saveColors(conn, tenantId, mock.colors); 41 | }); 42 | }).then(rdbEnv.end); 43 | }; 44 | 45 | commands.deleteFloor = function(floorId) { 46 | if (!floorId) { 47 | return Promise.reject('floorId is not defined.'); 48 | } 49 | var tenantId = ''; 50 | return rdbEnv.forConnectionAndTransaction(conn => { 51 | console.log('deleting floor ' + floorId + '...'); 52 | return db.deleteFloorWithObjects(conn, tenantId, floorId); 53 | }).then(rdbEnv.end); 54 | }; 55 | 56 | commands.deletePrototype = function(id) { 57 | if (!id) { 58 | return Promise.reject('id is not defined.'); 59 | } 60 | var tenantId = ''; 61 | return rdbEnv.forConnectionAndTransaction(conn => { 62 | console.log('deleting prototype ' + id + '...'); 63 | return db.deletePrototype(conn, tenantId, id); 64 | }).then(rdbEnv.end); 65 | }; 66 | 67 | commands.resetImage = function() { 68 | console.log('reseting image ...'); 69 | return db.resetImage(null, 'images/floors'); 70 | }; 71 | 72 | //------------------ 73 | // usage: 74 | // 75 | // node server/commands.js funcName 76 | // 77 | // 78 | 79 | var args = process.argv; 80 | args.shift(); // node 81 | args.shift(); // server/commands.js 82 | var funcName = args.shift(); 83 | if (funcName) { 84 | try { 85 | commands[funcName].apply(null, args).then(() => { 86 | console.log('done'); 87 | process.exit(0); 88 | }).catch((e) => { 89 | console.log(e); 90 | console.log(e.stack); 91 | process.exit(1); 92 | }); 93 | } catch (e) { 94 | console.log(e); 95 | console.log(e.stack); 96 | process.exit(1); 97 | } 98 | } 99 | 100 | module.exports = commands; 101 | -------------------------------------------------------------------------------- /src/elm/Model/FloorInfo.elm: -------------------------------------------------------------------------------- 1 | module Model.FloorInfo exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import CoreType exposing (..) 5 | import Model.Floor exposing (FloorBase) 6 | 7 | 8 | type FloorInfo 9 | = FloorInfo (Maybe FloorBase) FloorBase 10 | 11 | 12 | init : Maybe FloorBase -> FloorBase -> FloorInfo 13 | init publicFloor editingFloor = 14 | FloorInfo publicFloor editingFloor 15 | 16 | 17 | isNeverPublished : FloorInfo -> Bool 18 | isNeverPublished floorsInfo = 19 | case floorsInfo of 20 | FloorInfo Nothing _ -> 21 | True 22 | 23 | _ -> 24 | False 25 | 26 | 27 | idOf : FloorInfo -> FloorId 28 | idOf (FloorInfo publicFloor editingFloor) = 29 | editingFloor.id 30 | 31 | 32 | publicFloor : FloorInfo -> Maybe FloorBase 33 | publicFloor (FloorInfo publicFloor editingFloor) = 34 | publicFloor 35 | 36 | 37 | editingFloor : FloorInfo -> FloorBase 38 | editingFloor (FloorInfo publicFloor editingFloor) = 39 | editingFloor 40 | 41 | 42 | findPublicFloor : FloorId -> Dict FloorId FloorInfo -> Maybe FloorBase 43 | findPublicFloor floorId floorsInfo = 44 | floorsInfo 45 | |> findFloor floorId 46 | |> Maybe.andThen publicFloor 47 | 48 | 49 | findFloor : FloorId -> Dict FloorId FloorInfo -> Maybe FloorInfo 50 | findFloor floorId floorsInfo = 51 | floorsInfo 52 | |> Dict.get floorId 53 | 54 | 55 | mergeFloor : FloorBase -> Dict FloorId FloorInfo -> Dict FloorId FloorInfo 56 | mergeFloor editingFloor floorsInfo = 57 | floorsInfo 58 | |> Dict.update editingFloor.id (Maybe.map (mergeFloorHelp editingFloor)) 59 | 60 | 61 | mergeFloorHelp : FloorBase -> FloorInfo -> FloorInfo 62 | mergeFloorHelp floor (FloorInfo publicFloor editingFloor) = 63 | if floor.version < 0 then 64 | FloorInfo publicFloor floor 65 | else 66 | FloorInfo (Just floor) editingFloor 67 | 68 | 69 | toValues : Dict FloorId FloorInfo -> List FloorInfo 70 | toValues floorsInfo = 71 | floorsInfo 72 | |> Dict.toList 73 | |> List.map Tuple.second 74 | 75 | 76 | toPublicList : Dict FloorId FloorInfo -> List FloorBase 77 | toPublicList floorsInfo = 78 | floorsInfo 79 | |> toValues 80 | |> List.filterMap publicFloor 81 | |> List.sortBy .ord 82 | 83 | 84 | toEditingList : Dict FloorId FloorInfo -> List FloorBase 85 | toEditingList floorsInfo = 86 | floorsInfo 87 | |> toValues 88 | |> List.map editingFloor 89 | |> List.sortWith 90 | (\f1 f2 -> 91 | if f1.temporary == f2.temporary then 92 | intToOrd f1.ord f2.ord 93 | else if f2.temporary then 94 | GT 95 | else 96 | LT 97 | ) 98 | 99 | 100 | intToOrd : Int -> Int -> Order 101 | intToOrd i1 i2 = 102 | if i1 > i2 then 103 | GT 104 | else if i1 < i2 then 105 | LT 106 | else 107 | EQ 108 | 109 | 110 | sortByPublicOrder : Dict FloorId FloorInfo -> Maybe FloorInfo 111 | sortByPublicOrder floorsInfo = 112 | floorsInfo 113 | |> toValues 114 | |> List.sortBy (\info -> publicFloor info |> Maybe.map .ord |> Maybe.withDefault 999999) 115 | |> List.head 116 | -------------------------------------------------------------------------------- /src/elm/View/Icons.elm: -------------------------------------------------------------------------------- 1 | module View.Icons exposing (..) 2 | 3 | import Svg exposing (Svg) 4 | import Color exposing (Color, white, black, gray) 5 | import FontAwesome exposing (..) 6 | 7 | 8 | defaultColor : Color 9 | defaultColor = 10 | Color.rgb 140 140 140 11 | 12 | 13 | modeColor : Color 14 | modeColor = 15 | Color.rgb 90 90 90 16 | 17 | 18 | link : Svg msg 19 | link = 20 | FontAwesome.link defaultColor 16 21 | 22 | 23 | mode : (Color -> Int -> Svg msg) -> (Bool -> Svg msg) 24 | mode f = 25 | \selected -> 26 | f 27 | (if selected then 28 | white 29 | else 30 | modeColor 31 | ) 32 | 24 33 | 34 | 35 | selectMode : Bool -> Svg msg 36 | selectMode = 37 | mode mouse_pointer 38 | 39 | 40 | penMode : Bool -> Svg msg 41 | penMode = 42 | mode pencil 43 | 44 | 45 | stampMode : Bool -> Svg msg 46 | stampMode = 47 | mode th_large 48 | 49 | 50 | labelMode : Bool -> Svg msg 51 | labelMode = 52 | mode font 53 | 54 | 55 | personMatched : Float -> Svg msg 56 | personMatched ratio = 57 | check white (Basics.floor (18 * ratio)) 58 | 59 | 60 | personNotMatched : Float -> Svg msg 61 | personNotMatched ratio = 62 | question white (Basics.floor (18 * ratio)) 63 | 64 | 65 | popupClose : Svg msg 66 | popupClose = 67 | close defaultColor 18 68 | 69 | 70 | searchResultClose : Svg msg 71 | searchResultClose = 72 | close defaultColor 18 73 | 74 | 75 | proplabelColor : Color 76 | proplabelColor = 77 | defaultColor 78 | 79 | 80 | backgroundColorPropLabel : Svg msg 81 | backgroundColorPropLabel = 82 | th_large proplabelColor 12 83 | 84 | 85 | colorPropLabel : Svg msg 86 | colorPropLabel = 87 | font proplabelColor 12 88 | 89 | 90 | shapePropLabel : Svg msg 91 | shapePropLabel = 92 | star_o proplabelColor 12 93 | 94 | 95 | fontSizePropLabel : Svg msg 96 | fontSizePropLabel = 97 | font proplabelColor 12 98 | 99 | 100 | shapeRectangle : Svg msg 101 | shapeRectangle = 102 | square defaultColor 20 103 | 104 | 105 | shapeEllipse : Svg msg 106 | shapeEllipse = 107 | circle defaultColor 20 108 | 109 | 110 | searchResultItemPerson : Svg msg 111 | searchResultItemPerson = 112 | user defaultColor 20 113 | 114 | 115 | searchResultItemPost : Svg msg 116 | searchResultItemPost = 117 | user defaultColor 20 118 | 119 | 120 | personDetailPopupPersonTel : Svg msg 121 | personDetailPopupPersonTel = 122 | phone defaultColor 16 123 | 124 | 125 | personDetailPopupPersonMail : Svg msg 126 | personDetailPopupPersonMail = 127 | envelope defaultColor 16 128 | 129 | 130 | headerIconColor : Color 131 | headerIconColor = 132 | white 133 | 134 | 135 | editingToggle : Svg msg 136 | editingToggle = 137 | pencil headerIconColor 22 138 | 139 | 140 | printButton : Bool -> Svg msg 141 | printButton printMode = 142 | print 143 | (if printMode then 144 | defaultColor 145 | else 146 | headerIconColor 147 | ) 148 | 22 149 | 150 | 151 | helpButton : Svg msg 152 | helpButton = 153 | question_circle headerIconColor 22 154 | 155 | 156 | userMenuToggle : Bool -> Svg msg 157 | userMenuToggle open = 158 | (if open then 159 | caret_up 160 | else 161 | caret_down 162 | ) 163 | white 164 | 16 165 | -------------------------------------------------------------------------------- /src/elm/Page/Map/KeyOperation.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.KeyOperation exposing (..) 2 | 3 | import Char 4 | import Json.Decode as Decode exposing (Decoder) 5 | import CoreType exposing (..) 6 | import Page.Map.Msg exposing (Msg(..)) 7 | 8 | 9 | c : Int 10 | c = 11 | Char.toCode 'C' 12 | 13 | 14 | x : Int 15 | x = 16 | Char.toCode 'X' 17 | 18 | 19 | y : Int 20 | y = 21 | Char.toCode 'Y' 22 | 23 | 24 | z : Int 25 | z = 26 | Char.toCode 'Z' 27 | 28 | 29 | left : Int 30 | left = 31 | 37 32 | 33 | 34 | up : Int 35 | up = 36 | 38 37 | 38 | 39 | right : Int 40 | right = 41 | 39 42 | 43 | 44 | down : Int 45 | down = 46 | 40 47 | 48 | 49 | backSpace : Int 50 | backSpace = 51 | 8 52 | 53 | 54 | delete : Int 55 | delete = 56 | 46 57 | 58 | 59 | tab : Int 60 | tab = 61 | 9 62 | 63 | 64 | ctrl : Int 65 | ctrl = 66 | 17 67 | 68 | 69 | isCommand : Int -> Bool 70 | isCommand keyCode = 71 | (keyCode == 224) 72 | || (keyCode == 17) 73 | || (keyCode == 91) 74 | || (keyCode == 93) 75 | 76 | 77 | isCtrlOrCommand : Int -> Bool 78 | isCtrlOrCommand keyCode = 79 | keyCode == ctrl || isCommand keyCode 80 | 81 | 82 | decodeOperation : Decoder Msg 83 | decodeOperation = 84 | decodeOperationHelp toMsg 85 | 86 | 87 | toMsg : Bool -> Bool -> Int -> Maybe Msg 88 | toMsg ctrl shift keyCode = 89 | if ctrl && keyCode == c then 90 | Just Copy 91 | else if ctrl && keyCode == x then 92 | Just Cut 93 | else if ctrl && keyCode == y then 94 | Just Redo 95 | else if ctrl && keyCode == z then 96 | Just Undo 97 | else if keyCode == delete || keyCode == backSpace then 98 | Just Delete 99 | else if shift && keyCode == up then 100 | Just (ExpandOrShrinkToward Up) 101 | else if shift && keyCode == down then 102 | Just (ExpandOrShrinkToward Down) 103 | else if shift && keyCode == left then 104 | Just (ExpandOrShrinkToward Left) 105 | else if shift && keyCode == right then 106 | Just (ExpandOrShrinkToward Right) 107 | else if keyCode == up then 108 | Just (MoveSelecedObjectsToward Up) 109 | else if keyCode == down then 110 | Just (MoveSelecedObjectsToward Down) 111 | else if keyCode == left then 112 | Just (MoveSelecedObjectsToward Left) 113 | else if keyCode == right then 114 | Just (MoveSelecedObjectsToward Right) 115 | else if keyCode == tab then 116 | Just ShiftSelectionByTab 117 | else 118 | Nothing 119 | 120 | 121 | decodeOperationHelp : (Bool -> Bool -> Int -> Maybe msg) -> Decoder msg 122 | decodeOperationHelp toMsg = 123 | Decode.map3 toMsg 124 | decodeCtrlOrCommand 125 | decodeShift 126 | decodeKeyCode 127 | |> Decode.andThen 128 | (\maybeMsg -> 129 | maybeMsg 130 | |> Maybe.map Decode.succeed 131 | |> Maybe.withDefault (Decode.fail "") 132 | ) 133 | 134 | 135 | decodeShift : Decoder Bool 136 | decodeShift = 137 | Decode.field "shiftKey" Decode.bool 138 | 139 | 140 | decodeCtrlOrCommand : Decoder Bool 141 | decodeCtrlOrCommand = 142 | Decode.map2 (||) 143 | (Decode.field "ctrlKey" Decode.bool) 144 | (Decode.field "metaKey" Decode.bool) 145 | 146 | 147 | decodeKeyCode : Decoder Int 148 | decodeKeyCode = 149 | Decode.field "keyCode" Decode.int 150 | -------------------------------------------------------------------------------- /server/lib/mysql.js: -------------------------------------------------------------------------------- 1 | var mysql = require('mysql'); 2 | var log = require('./log.js'); 3 | 4 | function exec(conn, sql) { 5 | return new Promise((resolve, reject) => { 6 | conn.query(sql, (e, rows, fields) => { 7 | if (e) { 8 | log.system.error('SQL failed: ' + sql.split('\n').join(), e.message); 9 | reject(e); 10 | } else { 11 | (rows.length ? rows : []).forEach((row) => { 12 | (fields || []).forEach((field) => { 13 | if (field.type === 1) { 14 | row[field.name] = !!row[field.name]; 15 | } 16 | }); 17 | }); 18 | var _res = rows.length || rows.affectedRows || ''; 19 | log.system.debug(`${sql.split('\n').join()} => ${_res}`); 20 | resolve(rows); 21 | } 22 | }); 23 | }); 24 | } 25 | 26 | function one(conn, sql) { 27 | return exec(conn, sql).then((list) => { 28 | return Promise.resolve(list[0]); 29 | }); 30 | } 31 | 32 | function batch(conn, list) { 33 | var promises = list.map((sql) => { 34 | return exec(conn, sql); 35 | }); 36 | return promises.reduce((promise, next) => { 37 | return promise.then((result) => next); 38 | }, Promise.resolve()); 39 | } 40 | 41 | function beginTransaction(conn) { 42 | return new Promise((resolve, reject) => { 43 | conn.beginTransaction((e) => { 44 | if (e) { 45 | reject(e); 46 | } else { 47 | resolve(); 48 | } 49 | }); 50 | }); 51 | } 52 | 53 | function getConnection(pool) { 54 | return new Promise((resolve, reject) => { 55 | pool.getConnection((e, conn) => { 56 | if (e) { 57 | reject(e); 58 | } else { 59 | resolve(conn); 60 | } 61 | }); 62 | }); 63 | } 64 | 65 | function commit(conn) { 66 | return new Promise((resolve, reject) => { 67 | conn.commit((e) => { 68 | if (e) { 69 | reject(e); 70 | } else { 71 | resolve(); 72 | } 73 | }); 74 | }); 75 | } 76 | 77 | function rollback(conn) { 78 | return new Promise((resolve, reject) => { 79 | conn.rollback((e) => { 80 | if (e) { 81 | reject(e); 82 | } else { 83 | resolve(); 84 | } 85 | }); 86 | }); 87 | } 88 | 89 | function forTransaction(conn, f) { 90 | return beginTransaction(conn).then(() => { 91 | return f(conn).then((data) => { 92 | return commit(conn).then(() => { 93 | return Promise.resolve(data); 94 | }); 95 | }).catch((e) => { 96 | return rollback(conn).catch((e2) => { 97 | return Promise.reject([e, e2]); 98 | }).then(() => { 99 | return Promise.reject(e); 100 | }); 101 | }); 102 | }); 103 | } 104 | 105 | function createEnv(host, user, pass, dbname) { 106 | 107 | var pool = mysql.createPool({ 108 | host: host, 109 | user: user, 110 | password: pass, 111 | database: dbname, 112 | charset: 'utf8mb4' 113 | }); 114 | 115 | function forConnection(f) { 116 | return getConnection(pool).then((conn) => { 117 | return f(conn).then((data) => { 118 | conn.release(); 119 | return Promise.resolve(data); 120 | }).catch((e) => { 121 | conn.release(); 122 | return Promise.reject(e); 123 | }); 124 | }); 125 | } 126 | 127 | // maybe logic is incorrect 128 | function forConnectionAndTransaction(f) { 129 | return forConnection((conn) => { 130 | return forTransaction(conn, f); 131 | }); 132 | } 133 | 134 | function end() { 135 | return new Promise((resolve, reject) => { 136 | pool.end(e => { 137 | if (e) { 138 | reject(e); 139 | } else { 140 | resolve(); 141 | } 142 | }); 143 | }); 144 | } 145 | 146 | return { 147 | forConnection: forConnection, 148 | forConnectionAndTransaction: forConnectionAndTransaction, 149 | end: end 150 | }; 151 | } 152 | 153 | function escape(s) { 154 | return mysql.escape(s); 155 | } 156 | 157 | module.exports = { 158 | createEnv: createEnv, 159 | exec: exec, 160 | one: one, 161 | batch: batch, 162 | forTransaction: forTransaction, 163 | escape: escape 164 | }; 165 | -------------------------------------------------------------------------------- /src/elm/Page/Map/ContextMenu.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.ContextMenu exposing (view) 2 | 3 | import Dict 4 | import Html exposing (..) 5 | import ContextMenu 6 | import Model.User as User 7 | import Model.Object as Object 8 | import Model.EditingFloor as EditingFloor 9 | import Model.I18n as I18n 10 | import Model.Floor as Floor 11 | import Page.Map.Msg exposing (..) 12 | import Page.Map.Model as Model exposing (Model, DraggingContext(..)) 13 | import Page.Map.ContextMenuContext exposing (..) 14 | 15 | 16 | view : Model -> Html Msg 17 | view model = 18 | ContextMenu.view 19 | ContextMenu.defaultConfig 20 | ContextMenuMsg 21 | (toItemGroups model) 22 | model.contextMenu 23 | 24 | 25 | toItemGroups : Model -> ContextMenuContext -> List (List ( ContextMenu.Item, Msg )) 26 | toItemGroups model context = 27 | case context of 28 | ObjectContextMenu id -> 29 | let 30 | itemsForPerson = 31 | model.floor 32 | |> Maybe.andThen 33 | (\eFloor -> 34 | Floor.getObject id (EditingFloor.present eFloor) 35 | |> Maybe.andThen 36 | (\obj -> 37 | Object.relatedPerson obj 38 | |> Maybe.andThen 39 | (\personId -> 40 | Dict.get personId model.personInfo 41 | |> Maybe.map 42 | (\person -> 43 | [ ( ContextMenu.itemWithAnnotation (I18n.selectSamePost model.lang) person.post, SelectSamePost person.post ) 44 | , ( ContextMenu.itemWithAnnotation (I18n.searchSamePost model.lang) person.post, SearchByPost person.post ) 45 | ] 46 | ) 47 | ) 48 | ) 49 | ) 50 | 51 | forOneDesk = 52 | if [ id ] == model.selectedObjects then 53 | (Maybe.withDefault [] itemsForPerson) 54 | ++ [ ( ContextMenu.item (I18n.selectIsland model.lang), SelectIsland id ) 55 | , ( ContextMenu.item (I18n.selectSameColor model.lang), SelectSameColor id ) 56 | , ( ContextMenu.item (I18n.registerAsStamp model.lang), RegisterPrototype id ) 57 | ] 58 | else 59 | [] 60 | 61 | common = 62 | [ ( ContextMenu.item (I18n.pickupFirstWord model.lang), FirstNameOnly model.selectedObjects ) 63 | , ( ContextMenu.item (I18n.removeSpaces model.lang), RemoveSpaces model.selectedObjects ) 64 | , ( ContextMenu.item (I18n.rotate model.lang), RotateObjects model.selectedObjects ) 65 | ] 66 | 67 | items = 68 | forOneDesk ++ common 69 | in 70 | [ items ] 71 | 72 | FloorInfoContextMenu floorId -> 73 | if Maybe.map (EditingFloor.present >> .id) model.floor == Just floorId then 74 | if User.isGuest model.user then 75 | [] 76 | else if User.isAdmin model.user then 77 | [ [ ( ContextMenu.item (I18n.copyFloor model.lang), CopyFloor floorId False ) 78 | , ( ContextMenu.item (I18n.copyAndCreateTemporaryFloor model.lang), CopyFloor floorId True ) 79 | ] 80 | ] 81 | else 82 | [ [ ( ContextMenu.item (I18n.copyAndCreateTemporaryFloor model.lang), CopyFloor floorId True ) 83 | ] 84 | ] 85 | else 86 | [] 87 | -------------------------------------------------------------------------------- /src/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 119 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /server/static/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 119 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /server/lib/profile-service.js: -------------------------------------------------------------------------------- 1 | var request = require('request'); 2 | var log = require('./log.js'); 3 | 4 | function send(token, method, url, data) { 5 | return new Promise((resolve, reject) => { 6 | var options = { 7 | method: method, 8 | url: url, 9 | headers: { 10 | 'Authorization': token ? 'JWT ' + token : '' 11 | }, 12 | body: data, 13 | json: true 14 | }; 15 | request(options, function(e, response, body) { 16 | if (e || response.statusCode >= 400) { 17 | log.system.error(response ? response.statusCode : e, 'profile service: failed ' + method + ' ' + url); 18 | body && body.message && log.system.error(body.message); 19 | if (response && response.statusCode === 401) { 20 | reject(401); 21 | } else { 22 | reject(body ? body.message : e || response.statusCode); 23 | } 24 | } else { 25 | log.system.debug(response.statusCode, 'profile service: success ' + method + ' ' + url); 26 | resolve(body); 27 | } 28 | }); 29 | }); 30 | } 31 | 32 | function get(token, url) { 33 | return send(token, 'GET', url, null); 34 | } 35 | 36 | function post(token, url, data) { 37 | return send(token, 'POST', url, data); 38 | } 39 | 40 | function put(token, url, data) { 41 | return send(token, 'PUT', url, data); 42 | } 43 | 44 | function fixPerson(profile) { 45 | return { 46 | id: profile.userId, 47 | tenantId: profile.tenantId, 48 | name: profile.name, 49 | empNo: profile.employeeId, 50 | post: profile.post || '', //TODO 51 | tel1: profile.extensionPhone, 52 | tel2: profile.cellPhone, 53 | mail: profile.mail, 54 | image: profile.picture || '' //TODO or default.png 55 | }; 56 | } 57 | 58 | function getPerson(root, token, personId) { 59 | return get(token, root + '/1/profiles/' + personId).then((person) => { 60 | return Promise.resolve(fixPerson(person)); 61 | }).catch((e) => { 62 | if (e === 404) { 63 | return Promise.resolve(null); 64 | } 65 | return Promise.reject(e); 66 | }); 67 | } 68 | 69 | function getPeopleByIds(root, token, personIds) { 70 | return personIds.reduce((promise, personId) => { 71 | return promise.then(results => { 72 | return getPerson(root, token, personId).then(person => { 73 | if (person) { 74 | results.push(person); 75 | } 76 | return Promise.resolve(results); 77 | }); 78 | }) 79 | }, Promise.resolve([])); 80 | } 81 | 82 | function getPeopleByPost(root, token, post, exclusiveStartKey) { 83 | var url = root + '/1/profiles?q=' + encodeURIComponent(post) + 84 | (exclusiveStartKey ? '&exclusiveStartKey=' + exclusiveStartKey : '') 85 | return get(token, url).then((data) => { 86 | var people = data.profiles.map(fixPerson) 87 | if (data.lastEvaluatedKey) { 88 | return getPeopleByPost(root, token, post, data.lastEvaluatedKey).then((people2) => { 89 | return Promise.resolve(people.concat(people2)); 90 | }); 91 | } else { 92 | return Promise.resolve(people); 93 | } 94 | }); 95 | } 96 | 97 | function savePerson(root, token, person) { 98 | person.userId = person.mail; 99 | person.employeeId = person.empNo; 100 | person.ruby = person.ruby || null; 101 | person.cellPhone = person.tel || null; 102 | person.extensionPhone = person.tel || null; 103 | person.picture = person.image || null; 104 | person.organization = person.organization || null; //TODO 105 | person.post = person.post || null; 106 | person.rank = '' || null; 107 | person.workspace = '' || null; 108 | return put(token, root + '/1/profiles/' + encodeURIComponent(person.userId), person); 109 | } 110 | 111 | function search(root, token, query, exclusiveStartKey) { 112 | var url = root + '/1/profiles?q=' + encodeURIComponent(query) + 113 | (exclusiveStartKey ? '&exclusiveStartKey=' + exclusiveStartKey : ''); 114 | return get(token, url).then((data) => { 115 | var people = data.profiles.map(fixPerson); 116 | if (data.lastEvaluatedKey) { 117 | return search(root, token, query, data.lastEvaluatedKey).then((people2) => { 118 | return Promise.resolve(people.concat(people2)); 119 | }); 120 | } else { 121 | return Promise.resolve(people); 122 | } 123 | }); 124 | } 125 | 126 | module.exports = { 127 | getPerson: getPerson, 128 | getPeopleByPost: getPeopleByPost, 129 | getPeopleByIds: getPeopleByIds, 130 | savePerson: savePerson, 131 | search: search 132 | }; 133 | -------------------------------------------------------------------------------- /src/elm/Model/ObjectsChange.elm: -------------------------------------------------------------------------------- 1 | module Model.ObjectsChange exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import Model.Object as Object exposing (..) 5 | import CoreType exposing (..) 6 | 7 | 8 | type alias ObjectModification = 9 | { new : Object, old : Object, changes : List ObjectPropertyChange } 10 | 11 | 12 | type ObjectChange a 13 | = Added Object 14 | | Modified a 15 | | Deleted Object 16 | 17 | 18 | type alias ObjectsChange_ a = 19 | Dict ObjectId (ObjectChange a) 20 | 21 | 22 | type alias ObjectsChange = 23 | ObjectsChange_ Object 24 | 25 | 26 | type alias DetailedObjectsChange = 27 | ObjectsChange_ ObjectModification 28 | 29 | 30 | added : List Object -> ObjectsChange_ a 31 | added objects = 32 | objects 33 | |> List.map (\object -> ( Object.idOf object, Added object )) 34 | |> Dict.fromList 35 | 36 | 37 | modified : List ( ObjectId, a ) -> ObjectsChange_ a 38 | modified idRelatedList = 39 | idRelatedList 40 | |> List.map (\( id, a ) -> ( id, Modified a )) 41 | |> Dict.fromList 42 | 43 | 44 | empty : ObjectsChange 45 | empty = 46 | Dict.empty 47 | 48 | 49 | isEmpty : ObjectsChange_ a -> Bool 50 | isEmpty change = 51 | Dict.isEmpty change 52 | 53 | 54 | merge : Dict ObjectId Object -> ObjectsChange -> ObjectsChange -> ObjectsChange 55 | merge currentObjects new old = 56 | Dict.merge 57 | (\id new dict -> insertToMergedDict currentObjects id new dict) 58 | (\id new old dict -> 59 | case ( new, old ) of 60 | ( Deleted _, Added _ ) -> 61 | dict 62 | 63 | ( Modified object, Added _ ) -> 64 | insertToMergedDict currentObjects id (Added object) dict 65 | 66 | _ -> 67 | insertToMergedDict currentObjects id new dict 68 | ) 69 | (\id old dict -> insertToMergedDict currentObjects id old dict) 70 | new 71 | old 72 | Dict.empty 73 | 74 | 75 | insertToMergedDict : Dict ObjectId Object -> ObjectId -> ObjectChange Object -> ObjectsChange -> ObjectsChange 76 | insertToMergedDict currentObjects id value dict = 77 | currentObjects 78 | |> Dict.get id 79 | |> Maybe.map 80 | (\currentObject -> 81 | Dict.insert id (copyCurrentUpdateAtToObjects currentObject value) dict 82 | ) 83 | |> Maybe.withDefault (Dict.insert id value dict) 84 | 85 | 86 | 87 | -- current object does not exist if deleted 88 | 89 | 90 | copyCurrentUpdateAtToObjects : Object -> ObjectChange Object -> ObjectChange Object 91 | copyCurrentUpdateAtToObjects currentObject modification = 92 | case modification of 93 | Added object -> 94 | Added (Object.copyUpdateAt currentObject object) 95 | 96 | Modified object -> 97 | Modified (Object.copyUpdateAt currentObject object) 98 | 99 | Deleted object -> 100 | Deleted (Object.copyUpdateAt currentObject object) 101 | 102 | 103 | fromList : List ( ObjectId, ObjectChange a ) -> ObjectsChange_ a 104 | fromList list = 105 | Dict.fromList list 106 | 107 | 108 | toList : ObjectsChange_ a -> List (ObjectChange a) 109 | toList change = 110 | List.map Tuple.second (Dict.toList change) 111 | 112 | 113 | separate : ObjectsChange_ a -> { added : List Object, modified : List a, deleted : List Object } 114 | separate change = 115 | let 116 | ( added, modified, deleted ) = 117 | Dict.foldl 118 | (\_ value ( added, modified, deleted ) -> 119 | case value of 120 | Added object -> 121 | ( object :: added, modified, deleted ) 122 | 123 | Modified a -> 124 | ( added, a :: modified, deleted ) 125 | 126 | Deleted object -> 127 | ( added, modified, object :: deleted ) 128 | ) 129 | ( [], [], [] ) 130 | change 131 | in 132 | { added = added 133 | , modified = modified 134 | , deleted = deleted 135 | } 136 | 137 | 138 | simplify : DetailedObjectsChange -> ObjectsChange 139 | simplify change = 140 | change 141 | |> Dict.map 142 | (\id value -> 143 | case value of 144 | Added object -> 145 | Added object 146 | 147 | Modified mod -> 148 | Modified mod.new 149 | 150 | Deleted object -> 151 | Deleted object 152 | ) 153 | -------------------------------------------------------------------------------- /src/elm/View/CommonStyles.elm: -------------------------------------------------------------------------------- 1 | module View.CommonStyles exposing (..) 2 | 3 | import Util.StyleUtil exposing (..) 4 | import CoreType exposing (..) 5 | 6 | 7 | type alias S = 8 | List ( String, String ) 9 | 10 | 11 | invertedTextColor : String 12 | invertedTextColor = 13 | "#fff" 14 | 15 | 16 | inputTextColor : String 17 | inputTextColor = 18 | "#333" 19 | 20 | 21 | selectColor : String 22 | selectColor = 23 | "#69e" 24 | 25 | 26 | errorTextColor : String 27 | errorTextColor = 28 | "#d45" 29 | 30 | 31 | hoverBackgroundColor : String 32 | hoverBackgroundColor = 33 | "#9ce" 34 | 35 | 36 | noMargin : S 37 | noMargin = 38 | [ ( "margin", "0" ) ] 39 | 40 | 41 | noPadding : S 42 | noPadding = 43 | [ ( "padding", "0" ) ] 44 | 45 | 46 | noUserSelect : S 47 | noUserSelect = 48 | [ ( "user-select", "none" ) 49 | , ( "MozUserSelect", "none" ) -- must be UpperCamelCase 50 | , ( "-ms-user-select", "none" ) 51 | , ( "-webkit-user-select", "none" ) 52 | ] 53 | 54 | 55 | flex : S 56 | flex = 57 | [ ( "display", "flex" ) ] 58 | 59 | 60 | rect : Position -> Size -> S 61 | rect { x, y } { width, height } = 62 | [ ( "top", px y ) 63 | , ( "left", px x ) 64 | , ( "width", px width ) 65 | , ( "height", px height ) 66 | ] 67 | 68 | 69 | absoluteRect : Position -> Size -> S 70 | absoluteRect pos size = 71 | ( "position", "absolute" ) :: (rect pos size) 72 | 73 | 74 | card : S 75 | card = 76 | [ ( "padding", "20px" ) 77 | , ( "box-shadow", "rgba(0, 0, 0, 0.25) 0px 6px 4px" ) 78 | , ( "border-bottom", "solid 1px #ccc" ) 79 | ] 80 | 81 | 82 | formControl : S 83 | formControl = 84 | [ ( "margin-bottom", "6px" ) 85 | ] 86 | 87 | 88 | button : S 89 | button = 90 | [ ( "display", "block" ) 91 | , ( "text-align", "center" ) 92 | , ( "width", "100%" ) 93 | , ( "padding", "6px 12px" ) 94 | , ( "box-sizing", "border-box" ) 95 | , ( "font-size", "13px" ) 96 | , ( "cursor", "pointer" ) 97 | ] 98 | 99 | 100 | defaultButton : S 101 | defaultButton = 102 | button 103 | ++ [ ( "background-color", "#eee" ) 104 | , ( "border", "solid 1px #aaa" ) 105 | ] 106 | 107 | 108 | primaryButton : S 109 | primaryButton = 110 | button 111 | ++ [ ( "background-color", "rgb(100, 180, 85)" ) 112 | , ( "color", invertedTextColor ) 113 | , ( "border", "solid 1px rgb(100, 180, 85)" ) 114 | ] 115 | 116 | 117 | dangerButton : S 118 | dangerButton = 119 | button 120 | ++ [ ( "background-color", "rgb(221, 116, 116)" ) 121 | , ( "color", invertedTextColor ) 122 | , ( "border", "solid 1px rgb(221, 116, 116)" ) 123 | ] 124 | 125 | 126 | input : S 127 | input = 128 | [ ( "color", inputTextColor ) 129 | , ( "width", "100%" ) 130 | , ( "background-color", "#fff" ) 131 | , ( "box-shadow", "inset 0 1px 2px rgba(0,0,0,0.075)" ) 132 | , ( "border", "1px solid #ddd" ) 133 | , ( "padding", "6px 12px" ) 134 | , ( "box-sizing", "border-box" ) 135 | , ( "font-size", "13px" ) 136 | ] 137 | 138 | 139 | popup : Int -> String -> S 140 | popup padding position = 141 | [ ( "box-sizing", "border-box" ) 142 | , ( "padding", px padding ) 143 | , ( "background-color", "#fff" ) 144 | , ( "position", position ) 145 | ] 146 | 147 | 148 | modalBackground : Int -> S 149 | modalBackground zIndex = 150 | [ ( "background-color", "rgba(0,0,0,0.5)" ) 151 | , ( "position", "fixed" ) 152 | , ( "left", "0" ) 153 | , ( "right", "0" ) 154 | , ( "top", "0" ) 155 | , ( "bottom", "0" ) 156 | , ( "z-index", toString zIndex ) 157 | ] 158 | 159 | 160 | dialogWithSize : Int -> Int -> Int -> S 161 | dialogWithSize zIndex width height = 162 | popup 20 "fixed" 163 | ++ [ ( "margin", "auto" ) 164 | , ( "left", "0" ) 165 | , ( "right", "0" ) 166 | , ( "top", "0" ) 167 | , ( "bottom", "0" ) 168 | , ( "width", px width ) 169 | , ( "height", px height ) 170 | , ( "z-index", toString zIndex ) 171 | ] 172 | 173 | 174 | dialogWithMarginParcentage : Int -> Int -> Int -> S 175 | dialogWithMarginParcentage zIndex top left = 176 | popup 20 "fixed" 177 | ++ [ ( "left", percent left ) 178 | , ( "right", percent left ) 179 | , ( "top", percent top ) 180 | , ( "bottom", percent top ) 181 | , ( "z-index", toString zIndex ) 182 | ] 183 | 184 | 185 | dialogFooter : S 186 | dialogFooter = 187 | [ ( "position", "absolute" ) 188 | , ( "left", "0" ) 189 | , ( "bottom", "0" ) 190 | , ( "display", "flex" ) 191 | , ( "padding", px 20 ) 192 | , ( "width", "100%" ) 193 | , ( "box-sizing", "border-box" ) 194 | ] 195 | -------------------------------------------------------------------------------- /src/elm/Page/Master/PrototypeForm.elm: -------------------------------------------------------------------------------- 1 | module Page.Master.PrototypeForm exposing (..) 2 | 3 | import Model.Object exposing (Shape) 4 | import Model.Prototype exposing (Prototype) 5 | 6 | 7 | type alias PrototypeId = 8 | String 9 | 10 | 11 | type alias PrototypeForm = 12 | { id : PrototypeId 13 | , name : String 14 | , color : String 15 | , backgroundColor : String 16 | , width : String 17 | , height : String 18 | , fontSize : String 19 | , shape : Shape 20 | , personId : Maybe String 21 | } 22 | 23 | 24 | fromPrototype : Prototype -> PrototypeForm 25 | fromPrototype prototype = 26 | { id = prototype.id 27 | , name = prototype.name 28 | , color = prototype.color 29 | , backgroundColor = prototype.backgroundColor 30 | , width = toString prototype.width 31 | , height = toString prototype.height 32 | , fontSize = toString prototype.fontSize 33 | , shape = prototype.shape 34 | , personId = prototype.personId 35 | } 36 | 37 | 38 | toPrototype : PrototypeForm -> Result String Prototype 39 | toPrototype form = 40 | validateName form.name 41 | |> Result.andThen 42 | (\name -> 43 | validateColor form.color 44 | |> Result.andThen 45 | (\color -> 46 | validateBackgroundColor form.backgroundColor 47 | |> Result.andThen 48 | (\backgroundColor -> 49 | validateWidth form.width 50 | |> Result.andThen 51 | (\width -> 52 | validateHeight form.height 53 | |> Result.andThen 54 | (\height -> 55 | validateFontSize form.fontSize 56 | |> Result.map 57 | (\fontSize -> 58 | { id = form.id 59 | , name = name 60 | , color = color 61 | , backgroundColor = backgroundColor 62 | , width = width 63 | , height = height 64 | , fontSize = fontSize 65 | , shape = form.shape 66 | , personId = form.personId 67 | } 68 | ) 69 | ) 70 | ) 71 | ) 72 | ) 73 | ) 74 | 75 | 76 | validateName : String -> Result String String 77 | validateName name = 78 | Ok name 79 | 80 | 81 | validateColor : String -> Result String String 82 | validateColor color = 83 | Ok color 84 | 85 | 86 | validateBackgroundColor : String -> Result String String 87 | validateBackgroundColor backgroundColor = 88 | Ok backgroundColor 89 | 90 | 91 | validateFontSize : String -> Result String Float 92 | validateFontSize size = 93 | case String.toFloat size of 94 | Ok i -> 95 | if i >= 10 then 96 | Ok i 97 | else 98 | Err ("font size must not be less than 10") 99 | 100 | Err _ -> 101 | Err ("font size must be a number") 102 | 103 | 104 | validateWidth : String -> Result String Int 105 | validateWidth width = 106 | validateLength "width" width 107 | 108 | 109 | validateHeight : String -> Result String Int 110 | validateHeight height = 111 | validateLength "height" height 112 | 113 | 114 | validateLength : String -> String -> Result String Int 115 | validateLength fieldName length = 116 | case String.toInt length of 117 | Ok i -> 118 | if i > 0 && i % 8 == 0 then 119 | Ok i 120 | else 121 | Err (fieldName ++ " must be multiple of 8") 122 | 123 | Err _ -> 124 | Err (fieldName ++ " must be multiple of 8") 125 | -------------------------------------------------------------------------------- /src/elm/View/Theme.elm: -------------------------------------------------------------------------------- 1 | module View.Theme exposing (..) 2 | 3 | -- GLOBAL LIGHT THEME 4 | 5 | 6 | hoverBackgroundColor = 7 | "#9ce" 8 | 9 | 10 | normalTextColor = 11 | "#000" 12 | 13 | 14 | 15 | -- GLOBAL DARK THEME 16 | 17 | 18 | invertedText = 19 | "#fff" 20 | 21 | 22 | 23 | -- SELECT / HOVER 24 | 25 | 26 | selectColor = 27 | "#69e" 28 | 29 | 30 | 31 | -- ERROR 32 | 33 | 34 | errorTextColor = 35 | "#d45" 36 | 37 | 38 | 39 | -- HEADER 40 | 41 | 42 | headerBgColor = 43 | "rgb(100,100,120)" 44 | 45 | 46 | headerTextColor = 47 | "#eee" 48 | 49 | 50 | greetingImageBorder = 51 | "#888" 52 | 53 | 54 | userMenuBgColor = 55 | "rgb(100, 100, 120)" 56 | 57 | 58 | userMenuBoxShadow = 59 | "rgba(0, 0, 0, 0.2) 0px 3px 4px inset" 60 | 61 | 62 | 63 | -- STATUS BAR 64 | 65 | 66 | successBarBgColor = 67 | "#4c5" 68 | 69 | 70 | errorBarBgColor = 71 | "#d45" 72 | 73 | 74 | 75 | -- POPUP / Contextmenu 76 | 77 | 78 | popupBgColor = 79 | "#fff" 80 | 81 | 82 | modalBackgroundBgColor = 83 | "rgba(0,0,0,0.5)" 84 | 85 | 86 | 87 | -- BUTTON 88 | 89 | 90 | defaultButtonBgColor = 91 | "#eee" 92 | 93 | 94 | defaultButtonBorder = 95 | "#aaa" 96 | 97 | 98 | primaryButtonBgColor = 99 | "rgb(100, 180, 85)" 100 | 101 | 102 | primaryButtonBorder = 103 | "rgb(100, 180, 85)" 104 | 105 | 106 | 107 | -- OBJECT 108 | 109 | 110 | deskObjectBorderLighter = 111 | "rgba(100,100,100,0.3)" 112 | 113 | 114 | deskBorderDarker = 115 | "rgba(100,100,100,0.7)" 116 | 117 | 118 | labelObjectBgColorVisibleTransparent = 119 | "rgba(255,255,255,0.2)" 120 | 121 | 122 | labelObjectBorder = 123 | "rgba(100,100,100,0.3)" 124 | 125 | 126 | personMatchedBgColor = 127 | "#6a6" 128 | 129 | 130 | personNotMatchedBgColor = 131 | "#ccc" 132 | 133 | 134 | 135 | -- RELATED PERSON / CANDIDATE 136 | 137 | 138 | unsetRelatedPersonButtonBgColor = 139 | "#fff" 140 | 141 | 142 | unsetRelatedPersonButtonBorder = 143 | "#ddd" 144 | 145 | 146 | unsetRelatedPersonButtonHoverBgColor = 147 | "#a66" 148 | 149 | 150 | candidatesViewRelatedPersonBgColor = 151 | "#fff" 152 | 153 | 154 | candidatesViewRelatedPersonBorder = 155 | "#ddd" 156 | 157 | 158 | candidateItemBorderBottom = 159 | "#ddd" 160 | 161 | 162 | candidateItemBgColorSelected = 163 | hoverBackgroundColor 164 | 165 | 166 | candidateItemBgColor = 167 | "#fff" 168 | 169 | 170 | candidateItemHoverBgColor = 171 | hoverBackgroundColor 172 | 173 | 174 | 175 | -- CONTEXT MENU 176 | 177 | 178 | contextMenuBgColor = 179 | "#fff" 180 | 181 | 182 | contextMenuBorder = 183 | "#eee" 184 | 185 | 186 | contextMenuItemHover = 187 | "#9ce" 188 | 189 | 190 | 191 | -- CANVAS 192 | 193 | 194 | canvasViewBgColor = 195 | "#fff" 196 | 197 | 198 | canvasViewForPrintBgColor = 199 | "#fff" 200 | 201 | 202 | canvasContainerBgColor = 203 | "#fff" 204 | 205 | 206 | canvasContainerBgColorPrintMode = 207 | "#000" 208 | 209 | 210 | 211 | -- SUBVIEW 212 | 213 | 214 | subViewBackground = 215 | "#eee" 216 | 217 | 218 | subViewTabBgColorActive = 219 | "#eee" 220 | 221 | 222 | subViewTabBgColor = 223 | "#eee" 224 | 225 | 226 | subViewTabBoxShadow = 227 | "inset -4px 0 4px rgba(0,0,0,0.03)" 228 | 229 | 230 | 231 | -- MODE SELECTOR 232 | 233 | 234 | modeSelectionViewEachBorder = 235 | "#666" 236 | 237 | 238 | modeSelectionViewEachBgColor = 239 | selectColor 240 | 241 | 242 | modeSelectionViewEachTextColor = 243 | "#fff" 244 | 245 | 246 | 247 | -- PROPERTIES 248 | 249 | 250 | colorPropertyBorder = 251 | "#666" 252 | 253 | 254 | shapePropertyBorder = 255 | "#666" 256 | 257 | 258 | 259 | -- PROTOTYPE PREVIEW 260 | 261 | 262 | prototypePreviewViewBorderSelected = 263 | selectColor 264 | 265 | 266 | prototypePreviewViewBorder = 267 | "#666" 268 | 269 | 270 | prototypePreviewViewBgColor = 271 | "#fff" 272 | 273 | 274 | prototypePreviewScrollBgColor = 275 | "#ccc" 276 | 277 | 278 | 279 | -- TEXT INPUT 280 | 281 | 282 | inputTextColor = 283 | "#333" 284 | 285 | 286 | inputBgColor = 287 | "#fff" 288 | 289 | 290 | inputBorder = 291 | "#ddd" 292 | 293 | 294 | inputBoxShadow = 295 | "inset 0 1px 2px rgba(0,0,0,0.075)" 296 | 297 | 298 | 299 | -- FLOORS INFO 300 | 301 | 302 | floorsInfoViewItemBgColorPrivate = 303 | "#dbdbdb" 304 | 305 | 306 | floorsInfoViewItemBgColor = 307 | "#fff" 308 | 309 | 310 | floorsInfoViewItemBorderSelected = 311 | selectColor 312 | 313 | 314 | floorsInfoViewItemBorder = 315 | "#d0d0d0" 316 | 317 | 318 | floorsInfoViewItemHoverBgColorPrivate = 319 | "#ddd" 320 | 321 | 322 | floorsInfoViewItemHoverBgColor = 323 | "#eee" 324 | 325 | 326 | floorPropertyTextBorderBottom = 327 | "#aaa" 328 | 329 | 330 | loginContainerBorder = 331 | "#aaa" 332 | -------------------------------------------------------------------------------- /src/elm/Page/Map/SearchResultItemView.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.SearchResultItemView exposing (Item(..), view) 2 | 3 | import Time exposing (Time) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import CoreType exposing (..) 8 | import Model.I18n as I18n exposing (Language) 9 | import View.Icons as Icons 10 | import View.Styles as S 11 | 12 | 13 | type alias PostName = 14 | String 15 | 16 | 17 | type alias ObjectName = 18 | String 19 | 20 | 21 | type alias PersonName = 22 | String 23 | 24 | 25 | type alias FloorName = 26 | String 27 | 28 | 29 | 30 | -- VIEW MODEL 31 | 32 | 33 | type Item 34 | = Post PostName 35 | | Object ObjectId ObjectName FloorId FloorName (Maybe ( PersonId, PersonName )) Time Bool 36 | | MissingPerson PersonId PersonName 37 | 38 | 39 | view : Maybe FloorId -> msg -> Maybe (PersonId -> PersonName -> msg) -> Maybe (ObjectId -> String -> Maybe PersonId -> FloorId -> Time -> msg) -> Language -> Item -> Html msg 40 | view currentFloorId onSelect onStartDragMissing onStartDragExisting lang item = 41 | case item of 42 | Post postName -> 43 | wrapForNonDrag <| 44 | itemViewCommon postIcon <| 45 | div [] [ itemViewLabel (Just onSelect) False postName ] 46 | 47 | Object objectId _ floorId floorName (Just ( personId, personName )) updateAt focused -> 48 | let 49 | wrap = 50 | case onStartDragExisting of 51 | Just onStartDragExisting -> 52 | if currentFloorId == Just floorId then 53 | identity 54 | else 55 | wrapForDrag (onStartDragExisting objectId personName (Just personId) floorId updateAt) 56 | 57 | Nothing -> 58 | identity 59 | in 60 | wrap <| 61 | itemViewCommon personIcon <| 62 | div [] [ itemViewLabel (Just onSelect) focused (personName ++ "(" ++ floorName ++ ")") ] 63 | 64 | Object objectId objectName floorId floorName Nothing updateAt focused -> 65 | let 66 | wrap = 67 | case onStartDragExisting of 68 | Just onStartDragExisting -> 69 | wrapForDrag (onStartDragExisting objectId objectName Nothing floorId updateAt) 70 | 71 | Nothing -> 72 | identity 73 | in 74 | wrap <| 75 | itemViewCommon noIcon <| 76 | div [] [ itemViewLabel (Just onSelect) focused (objectName ++ "(" ++ floorName ++ ")") ] 77 | 78 | MissingPerson personId personName -> 79 | let 80 | wrap = 81 | case onStartDragMissing of 82 | Just onStartDragMissing -> 83 | wrapForDrag (onStartDragMissing personId personName) 84 | 85 | Nothing -> 86 | identity 87 | in 88 | wrap <| 89 | itemViewCommon personIcon <| 90 | div [] [ itemViewLabel Nothing False (personName ++ "(" ++ I18n.missing lang ++ ")") ] 91 | 92 | 93 | wrapForNonDrag : Html msg -> Html msg 94 | wrapForNonDrag child = 95 | div 96 | [ style (S.searchResultItem False) 97 | ] 98 | [ child ] 99 | 100 | 101 | wrapForDrag : msg -> Html msg -> Html msg 102 | wrapForDrag onStartDrag child = 103 | div 104 | [ onMouseDown onStartDrag 105 | , style (S.searchResultItem True) 106 | ] 107 | [ child ] 108 | 109 | 110 | itemViewCommon : Html msg -> Html msg -> Html msg 111 | itemViewCommon icon label = 112 | div 113 | [ style S.searchResultItemInner ] 114 | [ icon, label ] 115 | 116 | 117 | itemViewLabel : Maybe msg -> Bool -> String -> Html msg 118 | itemViewLabel onSelect focused s = 119 | let 120 | selectable = 121 | onSelect /= Nothing 122 | 123 | events = 124 | case onSelect of 125 | Just onSelect -> 126 | [ onClick onSelect ] 127 | 128 | Nothing -> 129 | [] 130 | in 131 | span 132 | (events ++ [ style (S.searchResultItemInnerLabel selectable focused) ]) 133 | [ text s ] 134 | 135 | 136 | personIcon : Html msg 137 | personIcon = 138 | div [ style S.searchResultItemIcon ] [ Icons.searchResultItemPerson ] 139 | 140 | 141 | postIcon : Html msg 142 | postIcon = 143 | div [ style S.searchResultItemIcon ] [ Icons.searchResultItemPost ] 144 | 145 | 146 | noIcon : Html msg 147 | noIcon = 148 | div [ style S.searchResultItemIcon ] [] 149 | -------------------------------------------------------------------------------- /src/elm/Page/Login/Main.elm: -------------------------------------------------------------------------------- 1 | port module Page.Login.Main exposing (..) 2 | 3 | import Html exposing (Html, text, div, input, form, h2) 4 | import Html.Attributes exposing (type_, value, action, method, style, autofocus) 5 | import Task 6 | import Http 7 | import Navigation 8 | import API.API as API 9 | import API.Page as Page 10 | import View.HeaderView as HeaderView 11 | import Util.HtmlUtil as HtmlUtil exposing (..) 12 | import Model.I18n as I18n exposing (Language(..)) 13 | import Page.Login.Styles as Styles 14 | 15 | 16 | port saveToken : String -> Cmd msg 17 | 18 | 19 | port tokenSaved : ({} -> msg) -> Sub msg 20 | 21 | 22 | main : Program Flags Model Msg 23 | main = 24 | Html.programWithFlags 25 | { init = init 26 | , view = view 27 | , update = update 28 | , subscriptions = \_ -> tokenSaved (always TokenSaved) 29 | } 30 | 31 | 32 | 33 | -------- 34 | 35 | 36 | type alias Model = 37 | { accountServiceRoot : String 38 | , title : String 39 | , error : Maybe String 40 | , inputId : String 41 | , inputPass : String 42 | , lang : Language 43 | } 44 | 45 | 46 | 47 | ---- 48 | 49 | 50 | type alias Flags = 51 | { accountServiceRoot : String 52 | , title : String 53 | , lang : String 54 | } 55 | 56 | 57 | type Msg 58 | = InputId String 59 | | InputPass String 60 | | Submit 61 | | Error Http.Error 62 | | Success String 63 | | TokenSaved 64 | 65 | 66 | init : Flags -> ( Model, Cmd Msg ) 67 | init { accountServiceRoot, title, lang } = 68 | { accountServiceRoot = accountServiceRoot 69 | , title = title 70 | , error = Nothing 71 | , inputId = "" 72 | , inputPass = "" 73 | , lang = 74 | if lang == "ja" then 75 | JA 76 | else 77 | EN 78 | } 79 | ! [] 80 | 81 | 82 | update : Msg -> Model -> ( Model, Cmd Msg ) 83 | update message model = 84 | case message of 85 | InputId s -> 86 | { model | inputId = s } ! [] 87 | 88 | InputPass s -> 89 | { model | inputPass = s } ! [] 90 | 91 | Submit -> 92 | let 93 | cmd = 94 | API.login 95 | model.accountServiceRoot 96 | model.inputId 97 | model.inputPass 98 | |> Task.map Success 99 | |> Task.onError (Error >> Task.succeed) 100 | |> Task.perform identity 101 | in 102 | model ! [ cmd ] 103 | 104 | Error e -> 105 | let 106 | _ = 107 | Debug.log "Error" e 108 | 109 | message = 110 | case e of 111 | Http.NetworkError -> 112 | -- "network error" 113 | "unauthorized" 114 | 115 | _ -> 116 | "unauthorized" 117 | in 118 | { model | error = Just message } ! [] 119 | 120 | Success token -> 121 | model ! [ saveToken token ] 122 | 123 | TokenSaved -> 124 | model ! [ Navigation.load Page.top ] 125 | 126 | 127 | 128 | ---- 129 | 130 | 131 | view : Model -> Html Msg 132 | view model = 133 | div 134 | [] 135 | [ HeaderView.view False model.title (Just ".") (text "") 136 | , container model 137 | ] 138 | 139 | 140 | container : Model -> Html Msg 141 | container model = 142 | div 143 | [ style Styles.loginContainer ] 144 | [ h2 [ style Styles.loginCaption ] [ text (I18n.signInTo model.lang model.title) ] 145 | , div [ style Styles.loginError ] [ text (Maybe.withDefault "" model.error) ] 146 | , loginForm model 147 | ] 148 | 149 | 150 | loginForm : Model -> Html Msg 151 | loginForm model = 152 | HtmlUtil.form_ Submit 153 | [] 154 | [ div 155 | [] 156 | [ div [] [ text (I18n.mailAddress model.lang) ] 157 | , input 158 | [ style Styles.formInput 159 | , onInput InputId 160 | , type_ "text" 161 | , value model.inputId 162 | , autofocus True 163 | ] 164 | [] 165 | ] 166 | , div 167 | [] 168 | [ div [] [ text (I18n.password model.lang) ] 169 | , input 170 | [ style Styles.formInput 171 | , onInput InputPass 172 | , type_ "password" 173 | , value model.inputPass 174 | ] 175 | [] 176 | ] 177 | , input 178 | [ style Styles.loginSubmitButton 179 | , type_ "submit" 180 | , value (I18n.signIn model.lang) 181 | ] 182 | [] 183 | ] 184 | -------------------------------------------------------------------------------- /src/elm/Util/HttpUtil.elm: -------------------------------------------------------------------------------- 1 | module Util.HttpUtil exposing (..) 2 | 3 | import Task exposing (..) 4 | import Native.HttpUtil 5 | import Util.File as File exposing (File(File)) 6 | import Http exposing (..) 7 | import Json.Decode as D exposing (Decoder) 8 | import Json.Encode as E 9 | 10 | 11 | encodeHeaders : List ( String, String ) -> E.Value 12 | encodeHeaders headers = 13 | E.list (List.map (\( k, v ) -> E.list [ E.string k, E.string v ]) headers) 14 | 15 | 16 | sendFile : String -> String -> List ( String, String ) -> File.File -> Task a () 17 | sendFile method url headers (File file) = 18 | Native.HttpUtil.sendFile method url (encodeHeaders headers) file 19 | 20 | 21 | makeUrl : String -> List ( String, String ) -> String 22 | makeUrl baseUrl args = 23 | case args of 24 | [] -> 25 | baseUrl 26 | 27 | _ -> 28 | baseUrl ++ "?" ++ String.join "&" (List.map queryPair args) 29 | 30 | 31 | queryPair : ( String, String ) -> String 32 | queryPair ( key, value ) = 33 | queryEscape key ++ "=" ++ queryEscape value 34 | 35 | 36 | queryEscape : String -> String 37 | queryEscape string = 38 | String.join "+" (String.split "%20" (Http.encodeUri string)) 39 | 40 | 41 | authorization : String -> Header 42 | authorization s = 43 | Http.header "Authorization" s 44 | 45 | 46 | authorizationTuple : String -> ( String, String ) 47 | authorizationTuple s = 48 | ( "Authorization", s ) 49 | 50 | 51 | sendJson : String -> Decoder value -> String -> List Header -> Http.Body -> Task Http.Error value 52 | sendJson method decoder url headers body = 53 | { method = method 54 | , headers = headers 55 | , url = url 56 | , body = body 57 | , expect = Http.expectJson decoder 58 | , timeout = Nothing 59 | , withCredentials = True 60 | } 61 | |> Http.request 62 | |> Http.toTask 63 | 64 | 65 | sendJsonNoResponse : String -> String -> List Header -> Http.Body -> Task Http.Error () 66 | sendJsonNoResponse method url headers body = 67 | { method = method 68 | , headers = headers 69 | , url = url 70 | , body = body 71 | , expect = Http.expectStringResponse (\_ -> Ok ()) 72 | , timeout = Nothing 73 | , withCredentials = True 74 | } 75 | |> Http.request 76 | |> Http.toTask 77 | 78 | 79 | get : Decoder value -> String -> List Header -> Task Http.Error value 80 | get decoder url headers = 81 | { method = "GET" 82 | , headers = headers 83 | , url = url 84 | , body = Http.emptyBody 85 | , expect = Http.expectJson decoder 86 | , timeout = Nothing 87 | , withCredentials = True 88 | } 89 | |> Http.request 90 | |> Http.toTask 91 | 92 | 93 | getWithoutCache : Decoder value -> String -> List Header -> Task Http.Error value 94 | getWithoutCache decoder url headers = 95 | let 96 | headers_ = 97 | [ Http.header "Pragma" "no-cache" 98 | , Http.header "Cache-Control" "no-cache" 99 | , Http.header "If-Modified-Since" "Thu, 01 Jun 1970 00:00:00 GMT" 100 | ] 101 | ++ headers 102 | in 103 | get decoder url headers_ 104 | 105 | 106 | postJson : Decoder value -> String -> List Header -> Http.Body -> Task Http.Error value 107 | postJson = 108 | sendJson "POST" 109 | 110 | 111 | postJsonNoResponse : String -> List Header -> Http.Body -> Task Http.Error () 112 | postJsonNoResponse = 113 | sendJsonNoResponse "POST" 114 | 115 | 116 | putJson : Decoder value -> String -> List Header -> Http.Body -> Task Http.Error value 117 | putJson = 118 | sendJson "PUT" 119 | 120 | 121 | putJsonNoResponse : String -> List Header -> Http.Body -> Task Http.Error () 122 | putJsonNoResponse = 123 | sendJsonNoResponse "PUT" 124 | 125 | 126 | patchJson : Decoder value -> String -> List Header -> Http.Body -> Task Http.Error value 127 | patchJson = 128 | sendJson "PATCH" 129 | 130 | 131 | patchJsonNoResponse : String -> List Header -> Http.Body -> Task Http.Error () 132 | patchJsonNoResponse = 133 | sendJsonNoResponse "PATCH" 134 | 135 | 136 | deleteJson : Decoder value -> String -> List Header -> Http.Body -> Task Http.Error value 137 | deleteJson = 138 | sendJson "DELETE" 139 | 140 | 141 | deleteJsonNoResponse : String -> List Header -> Http.Body -> Task Http.Error () 142 | deleteJsonNoResponse = 143 | sendJsonNoResponse "DELETE" 144 | 145 | 146 | recover404 : Task Http.Error a -> Task Http.Error (Maybe a) 147 | recover404 task = 148 | task 149 | |> Task.map Just 150 | |> Task.onError 151 | (\e -> 152 | case e of 153 | Http.BadStatus res -> 154 | if res.status.code == 404 then 155 | Task.succeed Nothing 156 | else 157 | Task.fail e 158 | 159 | e -> 160 | Task.fail e 161 | ) 162 | -------------------------------------------------------------------------------- /src/elm/Model/EditingFloor.elm: -------------------------------------------------------------------------------- 1 | module Model.EditingFloor exposing (..) 2 | 3 | import Model.Floor as Floor exposing (Floor) 4 | import Model.FloorDiff as FloorDiff 5 | import Model.ObjectsChange as ObjectsChange exposing (ObjectsChange) 6 | import Util.UndoList as UndoList exposing (UndoList) 7 | 8 | 9 | type alias EditingFloor = 10 | { undoList : UndoList Floor 11 | } 12 | 13 | 14 | init : Floor -> EditingFloor 15 | init floor = 16 | { undoList = UndoList.init floor 17 | } 18 | 19 | 20 | updateFloorAndObjects : (Floor -> Floor) -> EditingFloor -> ( EditingFloor, Floor, ObjectsChange ) 21 | updateFloorAndObjects f efloor = 22 | let 23 | floor = 24 | efloor.undoList.present 25 | 26 | newFloor = 27 | f floor 28 | 29 | propChanged = 30 | FloorDiff.diffPropertyChanges newFloor (Just floor) 31 | 32 | objectsChange = 33 | FloorDiff.diffObjects newFloor.objects floor.objects 34 | |> ObjectsChange.simplify 35 | 36 | changed = 37 | propChanged /= [] || (not <| ObjectsChange.isEmpty objectsChange) 38 | 39 | newUndoList = 40 | if changed then 41 | UndoList.new newFloor efloor.undoList 42 | else 43 | efloor.undoList 44 | in 45 | ( { efloor | undoList = newUndoList } 46 | , newFloor 47 | , objectsChange 48 | ) 49 | 50 | 51 | updateFloor : (Floor -> Floor) -> EditingFloor -> ( EditingFloor, Floor ) 52 | updateFloor f efloor = 53 | let 54 | floor = 55 | efloor.undoList.present 56 | 57 | newFloor = 58 | f floor 59 | 60 | propChanged = 61 | FloorDiff.diffPropertyChanges newFloor (Just floor) 62 | 63 | changed = 64 | propChanged /= [] 65 | 66 | newUndoList = 67 | if changed then 68 | UndoList.new newFloor efloor.undoList 69 | else 70 | efloor.undoList 71 | in 72 | ( { efloor | undoList = newUndoList } 73 | , newFloor 74 | ) 75 | 76 | 77 | updateObjects : (Floor -> Floor) -> EditingFloor -> ( EditingFloor, ObjectsChange ) 78 | updateObjects f efloor = 79 | let 80 | floor = 81 | efloor.undoList.present 82 | 83 | newFloor = 84 | f floor 85 | 86 | objectsChange = 87 | FloorDiff.diffObjects newFloor.objects floor.objects 88 | |> ObjectsChange.simplify 89 | 90 | changed = 91 | not <| ObjectsChange.isEmpty objectsChange 92 | 93 | newUndoList = 94 | if changed then 95 | UndoList.new newFloor efloor.undoList 96 | else 97 | efloor.undoList 98 | in 99 | ( { efloor | undoList = newUndoList }, objectsChange ) 100 | 101 | 102 | syncObjects : ObjectsChange -> EditingFloor -> EditingFloor 103 | syncObjects change efloor = 104 | let 105 | undoList = 106 | efloor.undoList 107 | 108 | separated = 109 | ObjectsChange.separate change 110 | 111 | -- Unsafe operation! 112 | newUndoList = 113 | { undoList 114 | | present = Floor.addObjects (separated.added ++ separated.modified) undoList.present 115 | } 116 | in 117 | { efloor | undoList = newUndoList } 118 | 119 | 120 | undo : EditingFloor -> ( EditingFloor, ObjectsChange ) 121 | undo efloor = 122 | let 123 | ( undoList, objectsChange ) = 124 | UndoList.undoReplace 125 | ObjectsChange.empty 126 | (\prev current -> 127 | let 128 | objectsChange = 129 | FloorDiff.diffObjects prev.objects current.objects 130 | in 131 | ( Floor.changeObjectsByChanges objectsChange current 132 | , objectsChange |> ObjectsChange.simplify 133 | ) 134 | ) 135 | efloor.undoList 136 | in 137 | ( { efloor | undoList = undoList }, objectsChange ) 138 | 139 | 140 | redo : EditingFloor -> ( EditingFloor, ObjectsChange ) 141 | redo efloor = 142 | let 143 | ( undoList, objectsChange ) = 144 | UndoList.redoReplace 145 | ObjectsChange.empty 146 | (\next current -> 147 | let 148 | objectsChange = 149 | FloorDiff.diffObjects next.objects current.objects 150 | in 151 | ( Floor.changeObjectsByChanges objectsChange current 152 | , objectsChange |> ObjectsChange.simplify 153 | ) 154 | ) 155 | efloor.undoList 156 | in 157 | ( { efloor | undoList = undoList }, objectsChange ) 158 | 159 | 160 | present : EditingFloor -> Floor 161 | present efloor = 162 | efloor.undoList.present 163 | -------------------------------------------------------------------------------- /src/elm/Model/SearchResult.elm: -------------------------------------------------------------------------------- 1 | module Model.SearchResult exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import CoreType exposing (..) 5 | import Model.Object as Object exposing (Object) 6 | import Model.Person exposing (Person) 7 | 8 | 9 | type alias PersonName = 10 | String 11 | 12 | 13 | type SearchResult 14 | = Object Object FloorId 15 | | MissingPerson PersonId 16 | 17 | 18 | type alias SearchResultsForOnePost = 19 | ( Maybe PersonName, List SearchResult ) 20 | 21 | 22 | getPersonId : SearchResult -> Maybe Id 23 | getPersonId result = 24 | case result of 25 | Object o _ -> 26 | Object.relatedPerson o 27 | 28 | MissingPerson personId -> 29 | Just personId 30 | 31 | 32 | groupByPostAndReorder : Maybe String -> Dict String Person -> List SearchResult -> List SearchResultsForOnePost 33 | groupByPostAndReorder thisFloorId personInfo results = 34 | groupByPost personInfo results 35 | |> List.map (\( maybePostName, results ) -> ( maybePostName, reorderResults thisFloorId results )) 36 | 37 | 38 | groupByPost : Dict String Person -> List SearchResult -> List SearchResultsForOnePost 39 | groupByPost personInfo results = 40 | results 41 | |> groupBy 42 | (\result -> 43 | getPersonId result 44 | |> Maybe.andThen (\id -> Dict.get id personInfo) 45 | |> Maybe.map (\person -> ( person.post, Just person.post )) 46 | |> Maybe.withDefault ( "", Nothing ) 47 | ) 48 | |> Dict.values 49 | 50 | 51 | groupBy : (a -> ( comparable, b )) -> List a -> Dict comparable ( b, List a ) 52 | groupBy f list = 53 | List.foldr 54 | (\a dict -> 55 | let 56 | ( key, realKey ) = 57 | f a 58 | in 59 | Dict.update key 60 | (\maybeValue -> 61 | case maybeValue of 62 | Just ( realKey, value ) -> 63 | Just ( realKey, a :: value ) 64 | 65 | Nothing -> 66 | Just ( realKey, [ a ] ) 67 | ) 68 | dict 69 | ) 70 | Dict.empty 71 | list 72 | 73 | 74 | reorderResults : Maybe String -> List SearchResult -> List SearchResult 75 | reorderResults thisFloorId results = 76 | let 77 | ( inThisFloor, inOtherFloor, missing ) = 78 | List.foldl 79 | (\result ( this, other, miss ) -> 80 | case result of 81 | Object _ floorId -> 82 | if Just floorId == thisFloorId then 83 | ( result :: this, other, miss ) 84 | else 85 | ( this, result :: other, miss ) 86 | 87 | MissingPerson _ -> 88 | ( this, other, result :: miss ) 89 | ) 90 | ( [], [], [] ) 91 | results 92 | in 93 | inThisFloor ++ inOtherFloor ++ missing 94 | 95 | 96 | mergeObjectInfo : String -> List Object -> List SearchResult -> List SearchResult 97 | mergeObjectInfo currentFloorId objects results = 98 | List.concatMap 99 | (\result -> 100 | case result of 101 | Object object floorId -> 102 | if floorId == currentFloorId then 103 | case List.filter (\o -> Object.idOf object == Object.idOf o) objects of 104 | [] -> 105 | case Object.relatedPerson object of 106 | Just personId -> 107 | [ MissingPerson personId ] 108 | 109 | Nothing -> 110 | [] 111 | 112 | _ -> 113 | [ result ] 114 | else 115 | [ result ] 116 | 117 | MissingPerson personId -> 118 | case List.filter (\object -> Object.relatedPerson object == Just personId) objects of 119 | [] -> 120 | [ result ] 121 | 122 | objects -> 123 | List.map (\object -> Object object currentFloorId) objects 124 | ) 125 | results 126 | 127 | 128 | moveObject : String -> List Object -> List SearchResult -> List SearchResult 129 | moveObject oldFloorId newObjects results = 130 | List.map 131 | (\result -> 132 | case result of 133 | Object object floorId -> 134 | if floorId == oldFloorId then 135 | case List.filter (\o -> Object.idOf object == Object.idOf o) newObjects of 136 | newObject :: _ -> 137 | Object newObject (Object.floorIdOf newObject) 138 | 139 | _ -> 140 | result 141 | else 142 | result 143 | 144 | MissingPerson personId -> 145 | result 146 | ) 147 | results 148 | -------------------------------------------------------------------------------- /src/elm/Page/Map/Msg.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.Msg exposing (..) 2 | 3 | import Time exposing (Time) 4 | import Debounce exposing (Debounce) 5 | import ContextMenu exposing (ContextMenu) 6 | import Model.Mode as Mode exposing (Mode(..), EditingMode(..)) 7 | import Model.User as User exposing (User) 8 | import Model.Person as Person exposing (Person) 9 | import Model.Object as Object exposing (..) 10 | import Model.ObjectsChange exposing (ObjectsChange) 11 | import Model.Prototype exposing (Prototype) 12 | import Model.Prototypes as Prototypes exposing (Prototypes) 13 | import Model.Floor as Floor exposing (Floor, FloorBase) 14 | import Model.FloorInfo as FloorInfo exposing (FloorInfo) 15 | import Model.Errors as Errors exposing (GlobalError(..)) 16 | import Model.I18n as I18n exposing (Language(..)) 17 | import Model.SearchResult as SearchResult exposing (SearchResult) 18 | import Model.SaveRequest as SaveRequest exposing (SaveRequest(..)) 19 | import Model.ColorPalette as ColorPalette exposing (ColorPalette) 20 | import Util.File exposing (File) 21 | import API.Cache as Cache exposing (Cache, UserState) 22 | import Component.FloorProperty as FloorProperty 23 | import Component.Header as Header exposing (..) 24 | import Component.ImageLoader as ImageLoader 25 | import Component.FloorDeleter as FloorDeleter 26 | import Page.Map.URL exposing (URL) 27 | import Page.Map.ContextMenuContext exposing (ContextMenuContext) 28 | import Page.Map.ClipboardOptionsView as ClipboardOptionsView 29 | import CoreType exposing (..) 30 | 31 | 32 | type alias Id = 33 | String 34 | 35 | 36 | 37 | {- TODO temporary here for sefe refactoring -} 38 | 39 | 40 | type ObjectNameInputMsg 41 | = NoOperation 42 | | CaretPosition Int 43 | | InputName ObjectId String Int 44 | | KeydownOnNameInput (List Person) ( Int, Int ) 45 | | KeyupOnNameInput Int 46 | | SelectCandidate ObjectId PersonId 47 | | UnsetPerson ObjectId 48 | 49 | 50 | type Msg 51 | = NoOp 52 | | UrlUpdate (Result String URL) 53 | | Initialized (Maybe String) Bool UserState User 54 | | FloorsInfoLoaded Bool (List FloorInfo) 55 | | FloorLoaded (Maybe Floor) 56 | | ColorsLoaded ColorPalette 57 | | PrototypesLoaded (List Prototype) 58 | | ImageSaved String Int Int 59 | | RequestSave SaveRequest 60 | | SaveFloorDebounceMsg Debounce.Msg 61 | | ObjectsSaved ObjectsChange 62 | | UnlockSaveFloor 63 | | FloorSaved FloorBase 64 | | FloorPublished Floor 65 | | ClickOnCanvas 66 | | MouseDownOnCanvas Position 67 | | MouseUpOnCanvas Position 68 | | FocusCanvas 69 | | MouseDownOnObject Bool Bool Id Position 70 | | MouseUpOnObject Id Position 71 | | MouseDownOnResizeGrip Id Position 72 | | StartEditObject Id 73 | | Ctrl Bool 74 | | SelectBackgroundColor String 75 | | SelectColor String 76 | | SelectShape Object.Shape 77 | | SelectFontSize Float 78 | | InputObjectUrl (List ObjectId) String 79 | | ObjectNameInputMsg ObjectNameInputMsg 80 | | BeforeContextMenuOnObject Id Msg 81 | | ContextMenuMsg (ContextMenu.Msg ContextMenuContext) 82 | | GoToFloor String Bool 83 | | SelectSamePost String 84 | | SearchByPost String 85 | | GotSamePostPeople (List Person) 86 | | SelectIsland Id 87 | | SelectSameColor Id 88 | | WindowSize Size 89 | | MouseWheel Float Position 90 | | ChangeMode EditingMode 91 | | PrototypesMsg Prototypes.Msg 92 | | ClipboardOptionsMsg ( ClipboardOptionsView.Form, Maybe Size ) 93 | | RegisterPrototype Id 94 | | FloorPropertyMsg FloorProperty.Msg 95 | | RotateObjects (List ObjectId) 96 | | FirstNameOnly (List Id) 97 | | RemoveSpaces (List Id) 98 | | HeaderMsg Header.Msg 99 | | SignIn 100 | | SignOut 101 | | ToggleEditing 102 | | TogglePrintView 103 | | SelectLang Language 104 | | UpdateSearchQuery String 105 | | SubmitSearch 106 | | GotSearchResult (List SearchResult) (List Person) 107 | | SelectSearchResult ObjectId FloorId (Maybe PersonId) 108 | | CloseSearchResult 109 | | StartDraggingFromMissingPerson String String 110 | | StartDraggingFromExistingObject Id String (Maybe String) String Time 111 | | CachePeople (List Person) 112 | | RequestCandidate Id String 113 | | SearchCandidateDebounceMsg Debounce.Msg 114 | | GotCandidateSelection Id (List Person) 115 | | GotMatchingList (List ( Id, List Person )) 116 | | UpdatePersonCandidate Id (List Id) 117 | | PreparePublish 118 | | GotDiffSource ( Floor, Maybe Floor ) 119 | | CloseDiff 120 | | ConfirmDiff 121 | | ClosePopup 122 | | ShowDetailForObject Id 123 | | CreateNewFloor 124 | | CopyFloor FloorId Bool 125 | | EmulateClick Id Bool Time 126 | | TokenRemoved 127 | | Undo 128 | | Redo 129 | | Focused 130 | | PasteFromClipboard String 131 | | SyncFloor 132 | | MouseMove Position 133 | | MouseUp 134 | | ImageLoaderMsg ImageLoader.Msg 135 | | GotFileWithDataURL File String 136 | | FloorDeleterMsg FloorDeleter.Msg 137 | | FloorDeleted Floor 138 | | InsertEmoji String 139 | | ChangeToObjectUrl ObjectId 140 | | SetTransition Bool 141 | | Copy 142 | | Cut 143 | | Delete 144 | | MoveSelecedObjectsToward Direction 145 | | ShiftSelectionByTab 146 | | ExpandOrShrinkToward Direction 147 | | Print 148 | | FlipFloor 149 | | Error GlobalError 150 | -------------------------------------------------------------------------------- /src/elm/Model/FloorDiff.elm: -------------------------------------------------------------------------------- 1 | module Model.FloorDiff exposing (..) 2 | 3 | import Dict exposing (Dict) 4 | import CoreType exposing (..) 5 | import Model.Object as Object exposing (..) 6 | import Model.Floor as Floor exposing (Floor) 7 | import Model.ObjectsChange as ObjectsChange exposing (..) 8 | 9 | 10 | type alias Options msg = 11 | { onClose : msg 12 | , onConfirm : msg 13 | , noOp : msg 14 | } 15 | 16 | 17 | type alias PropChanges = 18 | List ( String, String, String ) 19 | 20 | 21 | diff : Floor -> Maybe Floor -> ( PropChanges, DetailedObjectsChange ) 22 | diff new old = 23 | ( diffPropertyChanges new old 24 | , diffObjects new.objects (Maybe.withDefault Dict.empty (Maybe.map .objects old)) 25 | ) 26 | 27 | 28 | diffPropertyChanges : Floor -> Maybe Floor -> List ( String, String, String ) 29 | diffPropertyChanges current prev = 30 | case prev of 31 | Just prev -> 32 | propertyChangesHelp current prev 33 | 34 | -- FIXME completely wrong 35 | Nothing -> 36 | (if Floor.name current /= "" then 37 | [ ( "Name", Floor.name current, "" ) ] 38 | else 39 | [] 40 | ) 41 | ++ (case current.realSize of 42 | Just ( w2, h2 ) -> 43 | [ ( "Size", "(" ++ toString w2 ++ ", " ++ toString h2 ++ ")", "" ) ] 44 | 45 | Nothing -> 46 | [] 47 | ) 48 | 49 | 50 | propertyChangesHelp : Floor -> Floor -> List ( String, String, String ) 51 | propertyChangesHelp current prev = 52 | let 53 | nameChange = 54 | if Floor.name current == Floor.name prev then 55 | [] 56 | else 57 | [ ( "Name", Floor.name current, Floor.name prev ) ] 58 | 59 | ordChange = 60 | if current.ord == prev.ord then 61 | [] 62 | else 63 | [ ( "Order", toString current.ord, toString prev.ord ) ] 64 | 65 | sizeChange = 66 | if current.realSize == prev.realSize then 67 | [] 68 | else 69 | case ( current.realSize, prev.realSize ) of 70 | ( Just ( w1, h1 ), Just ( w2, h2 ) ) -> 71 | [ ( "Size", "(" ++ toString w1 ++ ", " ++ toString h1 ++ ")", "(" ++ toString w2 ++ ", " ++ toString h2 ++ ")" ) ] 72 | 73 | ( Just ( w1, h1 ), Nothing ) -> 74 | [ ( "Size", "(" ++ toString w1 ++ ", " ++ toString h1 ++ ")", "" ) ] 75 | 76 | ( Nothing, Just ( w2, h2 ) ) -> 77 | [ ( "Size", "", "(" ++ toString w2 ++ ", " ++ toString h2 ++ ")" ) ] 78 | 79 | _ -> 80 | [] 81 | 82 | -- should not happen 83 | imageChange = 84 | if current.image /= prev.image then 85 | [ ( "Image", Maybe.withDefault "" current.image, Maybe.withDefault "" prev.image ) ] 86 | else 87 | [] 88 | 89 | flipImageChange = 90 | if current.flipImage /= prev.flipImage then 91 | [ ( "FlipImage", toString current.flipImage, toString prev.flipImage ) ] 92 | else 93 | [] 94 | in 95 | nameChange ++ ordChange ++ sizeChange ++ imageChange ++ flipImageChange 96 | 97 | 98 | diffObjects : Dict ObjectId Object -> Dict ObjectId Object -> DetailedObjectsChange 99 | diffObjects newObjects oldObjects = 100 | Dict.merge 101 | (\id new dict -> Dict.insert id (ObjectsChange.Added new) dict) 102 | (\id new old dict -> 103 | case diffObjectProperty new old of 104 | [] -> 105 | dict 106 | 107 | list -> 108 | Dict.insert id (ObjectsChange.Modified { new = Object.copyUpdateAt old new, old = old, changes = list }) dict 109 | ) 110 | (\id old dict -> Dict.insert id (ObjectsChange.Deleted old) dict) 111 | newObjects 112 | oldObjects 113 | Dict.empty 114 | 115 | 116 | diffObjectProperty : Object -> Object -> List ObjectPropertyChange 117 | diffObjectProperty new old = 118 | List.filterMap 119 | identity 120 | [ objectPropertyChange ChangeName Object.nameOf new old 121 | , objectPropertyChange ChangeSize Object.sizeOf new old 122 | , objectPropertyChange ChangePosition Object.positionOf new old 123 | , objectPropertyChange ChangeBackgroundColor Object.backgroundColorOf new old 124 | , objectPropertyChange ChangeColor Object.colorOf new old 125 | , objectPropertyChange ChangeFontSize Object.fontSizeOf new old 126 | , objectPropertyChange ChangeBold Object.isBold new old 127 | , objectPropertyChange ChangeUrl Object.urlOf new old 128 | , objectPropertyChange ChangeShape Object.shapeOf new old 129 | , objectPropertyChange ChangePerson Object.relatedPerson new old 130 | ] 131 | 132 | 133 | objectPropertyChange : (a -> a -> b) -> (Object -> a) -> Object -> Object -> Maybe b 134 | objectPropertyChange f toProperty new old = 135 | let 136 | newProp = 137 | toProperty new 138 | 139 | oldProp = 140 | toProperty old 141 | in 142 | if newProp /= oldProp then 143 | Just (f newProp oldProp) 144 | else 145 | Nothing 146 | 147 | 148 | 149 | -- 150 | -------------------------------------------------------------------------------- /src/elm/Page/Map/FloorsInfoView.elm: -------------------------------------------------------------------------------- 1 | module Page.Map.FloorsInfoView exposing (view) 2 | 3 | import Dict exposing (Dict) 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import ContextMenu 8 | import CoreType exposing (..) 9 | import View.CommonStyles as CommonStyles 10 | import View.Styles as Styles 11 | import Model.User as User exposing (User) 12 | import Model.FloorInfo as FloorInfo exposing (FloorInfo(..)) 13 | import Page.Map.Msg exposing (Msg(..)) 14 | import Page.Map.ContextMenuContext as ContextMenuContext 15 | 16 | 17 | view : Bool -> User -> Bool -> Maybe String -> Dict FloorId FloorInfo -> Html Msg 18 | view disableContextmenu user isEditMode currentFloorId floorsInfo = 19 | if isEditMode then 20 | viewEditingFloors disableContextmenu user currentFloorId floorsInfo 21 | else 22 | viewPublicFloors currentFloorId floorsInfo 23 | 24 | 25 | viewEditingFloors : Bool -> User -> Maybe FloorId -> Dict FloorId FloorInfo -> Html Msg 26 | viewEditingFloors disableContextmenu user currentFloorId floorsInfo = 27 | let 28 | contextMenuMsg floor = 29 | if not disableContextmenu && not (User.isGuest user) then 30 | Just (ContextMenu.open ContextMenuMsg (ContextMenuContext.FloorInfoContextMenu floor.id)) 31 | else 32 | Nothing 33 | 34 | floorList = 35 | floorsInfo 36 | |> FloorInfo.toValues 37 | |> List.map (\floorInfo -> ( FloorInfo.isNeverPublished floorInfo, FloorInfo.editingFloor floorInfo )) 38 | |> List.sortBy (Tuple.second >> .ord) 39 | |> List.map 40 | (\( isNeverPublished, floor ) -> 41 | eachView 42 | (contextMenuMsg floor) 43 | (GoToFloor floor.id True) 44 | (currentFloorId == Just floor.id) 45 | (if floor.temporary then 46 | Temporary 47 | else if isNeverPublished then 48 | Private 49 | else 50 | Public 51 | ) 52 | floor.name 53 | ) 54 | 55 | create = 56 | if User.isAdmin user then 57 | [ createButton ] 58 | else 59 | [] 60 | in 61 | wrapList (floorList ++ create) 62 | 63 | 64 | viewPublicFloors : Maybe FloorId -> Dict FloorId FloorInfo -> Html Msg 65 | viewPublicFloors currentFloorId floorsInfo = 66 | floorsInfo 67 | |> FloorInfo.toPublicList 68 | |> List.map 69 | (\floor -> 70 | eachView 71 | Nothing 72 | (GoToFloor floor.id False) 73 | (currentFloorId == Just floor.id) 74 | Public 75 | floor.name 76 | ) 77 | |> wrapList 78 | 79 | 80 | wrapList : List (Html msg) -> Html msg 81 | wrapList children = 82 | ul [ style floorsInfoViewStyle ] children 83 | 84 | 85 | eachView : Maybe (Attribute msg) -> msg -> Bool -> ColorType -> String -> Html msg 86 | eachView maybeOpenContextMenu onClickMsg selected colorType floorName = 87 | li 88 | (style (floorsInfoViewItemStyle selected colorType) 89 | :: onClick onClickMsg 90 | :: (maybeOpenContextMenu |> Maybe.map (List.singleton) |> Maybe.withDefault []) 91 | ) 92 | [ span [ style floorsInfoViewItemLinkStyle ] [ text floorName ] ] 93 | 94 | 95 | createButton : Html Msg 96 | createButton = 97 | li 98 | [ style (floorsInfoViewItemStyle False Public) 99 | , onClick CreateNewFloor 100 | ] 101 | [ span [ style floorsInfoViewItemLinkStyle ] [ text "+" ] ] 102 | 103 | 104 | type alias Styles = 105 | List ( String, String ) 106 | 107 | 108 | floorsInfoViewStyle : Styles 109 | floorsInfoViewStyle = 110 | [ ( "position", "absolute" ) 111 | , ( "width", "calc(100% - 300px)" ) 112 | , ( "z-index", Styles.zFloorInfo ) 113 | ] 114 | 115 | 116 | type ColorType 117 | = Public 118 | | Private 119 | | Temporary 120 | 121 | 122 | floorsInfoViewItemStyle : Bool -> ColorType -> Styles 123 | floorsInfoViewItemStyle selected colorType = 124 | [ ( "background-color" 125 | , case colorType of 126 | Public -> 127 | "#fff" 128 | 129 | Private -> 130 | "#dbdbdb" 131 | 132 | Temporary -> 133 | "#dbdbaa" 134 | ) 135 | , ( "border-right" 136 | , if selected then 137 | "solid 2px " ++ CommonStyles.selectColor 138 | else 139 | "solid 1px #d0d0d0" 140 | ) 141 | , ( "border-bottom" 142 | , if selected then 143 | "solid 2px " ++ CommonStyles.selectColor 144 | else 145 | "solid 1px #d0d0d0" 146 | ) 147 | , ( "border-top" 148 | , if selected then 149 | "solid 2px " ++ CommonStyles.selectColor 150 | else 151 | "none" 152 | ) 153 | , ( "border-left" 154 | , if selected then 155 | "solid 2px " ++ CommonStyles.selectColor 156 | else 157 | "none" 158 | ) 159 | , ( "min-width", "72px" ) 160 | , ( "box-sizing", "border-box" ) 161 | , ( "height", "30px" ) 162 | , ( "position", "relative" ) 163 | , ( "font-size", "12px" ) 164 | , ( "float", "left" ) 165 | , ( "cursor", "pointer" ) 166 | ] 167 | 168 | 169 | floorsInfoViewItemLinkStyle : Styles 170 | floorsInfoViewItemLinkStyle = 171 | [ ( "display", "block" ) 172 | , ( "text-align", "center" ) 173 | , ( "vertical-align", "middle" ) 174 | , ( "line-height", "30px" ) 175 | , ( "padding", "0 8px" ) 176 | ] 177 | -------------------------------------------------------------------------------- /src/elm/Model/Prototypes.elm: -------------------------------------------------------------------------------- 1 | module Model.Prototypes exposing (..) 2 | 3 | import Model.Prototype exposing (Prototype) 4 | import Model.ObjectsOperation as ObjectsOperation 5 | import Util.ListUtil exposing (..) 6 | import CoreType exposing (..) 7 | 8 | 9 | type alias PositionedPrototype = 10 | ( Prototype, Position ) 11 | 12 | 13 | type alias Prototypes = 14 | { data : List Prototype 15 | , selected : Int 16 | } 17 | 18 | 19 | gridSize : Int 20 | gridSize = 21 | 8 22 | 23 | 24 | 25 | --TODO 26 | 27 | 28 | init : List Prototype -> Prototypes 29 | init data = 30 | { data = data 31 | , selected = 0 -- index 32 | } 33 | 34 | 35 | type Msg 36 | = SelectPrev 37 | | SelectNext 38 | 39 | 40 | prev : Msg 41 | prev = 42 | SelectPrev 43 | 44 | 45 | next : Msg 46 | next = 47 | SelectNext 48 | 49 | 50 | update : Msg -> Prototypes -> Prototypes 51 | update msg model = 52 | case msg of 53 | SelectPrev -> 54 | { model 55 | | selected = max 0 (model.selected - 1) -- fail safe 56 | } 57 | 58 | SelectNext -> 59 | { model 60 | | selected = min (List.length model.data - 1) (model.selected + 1) -- fail safe 61 | } 62 | 63 | 64 | register : Prototype -> Prototypes -> Prototypes 65 | register prototype model = 66 | let 67 | newPrototypes = 68 | model.data ++ [ prototype ] 69 | in 70 | { model 71 | | data = newPrototypes 72 | , selected = List.length newPrototypes - 1 73 | } 74 | 75 | 76 | selectedPrototype : Prototypes -> Prototype 77 | selectedPrototype model = 78 | findPrototypeByIndex model.selected model.data 79 | 80 | 81 | findPrototypeByIndex : Int -> List Prototype -> Prototype 82 | findPrototypeByIndex index list = 83 | case getAt index list of 84 | Just prototype -> 85 | prototype 86 | 87 | Nothing -> 88 | case List.head list of 89 | Just prototype -> 90 | prototype 91 | 92 | Nothing -> 93 | Debug.crash "no prototypes found" 94 | 95 | 96 | prototypes : Prototypes -> List ( Prototype, Bool ) 97 | prototypes model = 98 | model.data 99 | |> List.indexedMap 100 | (\index prototype -> 101 | ( prototype, model.selected == index ) 102 | ) 103 | 104 | 105 | stampIndices : Bool -> Size -> Position -> Position -> ( List Int, List Int ) 106 | stampIndices horizontal deskSize pos1 pos2 = 107 | let 108 | ( amountX, amountY ) = 109 | if horizontal then 110 | let 111 | amountX = 112 | (abs (pos2.x - pos1.x) + deskSize.width // 2) // deskSize.width 113 | 114 | amountY = 115 | if abs (pos2.y - pos1.y) > (deskSize.height // 2) then 116 | 1 117 | else 118 | 0 119 | in 120 | ( amountX, amountY ) 121 | else 122 | let 123 | amountX = 124 | if abs (pos2.x - pos1.x) > (deskSize.width // 2) then 125 | 1 126 | else 127 | 0 128 | 129 | amountY = 130 | (abs (pos2.y - pos1.y) + deskSize.height // 2) // deskSize.height 131 | in 132 | ( amountX, amountY ) 133 | in 134 | ( List.map 135 | (\i -> 136 | if pos2.x > pos1.x then 137 | i 138 | else 139 | -i 140 | ) 141 | (List.range 0 amountX) 142 | , List.map 143 | (\i -> 144 | if pos2.y > pos1.y then 145 | i 146 | else 147 | -i 148 | ) 149 | (List.range 0 amountY) 150 | ) 151 | 152 | 153 | generateAllCandidatePosition : Size -> Position -> ( List Int, List Int ) -> List Position 154 | generateAllCandidatePosition deskSize centerPos ( indicesX, indicesY ) = 155 | let 156 | lefts = 157 | List.map (\index -> centerPos.x + deskSize.width * index) indicesX 158 | 159 | tops = 160 | List.map (\index -> centerPos.y + deskSize.height * index) indicesY 161 | in 162 | List.concatMap (\left -> List.map (\top -> Position left top) tops) lefts 163 | 164 | 165 | positionedPrototypesOnDragging : Int -> Prototype -> Position -> Position -> List PositionedPrototype 166 | positionedPrototypesOnDragging gridSize prototype xy1 xy2 = 167 | -- imagePos 168 | let 169 | x1 = 170 | xy1.x 171 | 172 | y1 = 173 | xy1.y 174 | 175 | x2 = 176 | xy2.x 177 | 178 | y2 = 179 | xy2.y 180 | 181 | deskSize = 182 | ( prototype.width, prototype.height ) 183 | 184 | flip ( w, h ) = 185 | ( h, w ) 186 | 187 | horizontal = 188 | abs (x2 - x1) > abs (y2 - y1) 189 | 190 | ( deskWidth, deskHeight ) = 191 | if horizontal then 192 | flip deskSize 193 | else 194 | deskSize 195 | 196 | ( indicesX, indicesY ) = 197 | stampIndices horizontal (Size deskWidth deskHeight) xy1 xy2 198 | 199 | center = 200 | ObjectsOperation.fitPositionToGrid 201 | gridSize 202 | (Position (x1 - Tuple.first deskSize // 2) (y1 - Tuple.second deskSize // 2)) 203 | 204 | all = 205 | generateAllCandidatePosition 206 | (Size deskWidth deskHeight) 207 | center 208 | ( indicesX, indicesY ) 209 | 210 | prototype_ = 211 | { prototype 212 | | width = deskWidth 213 | , height = deskHeight 214 | } 215 | in 216 | List.map ((,) prototype_) all 217 | --------------------------------------------------------------------------------