// Copyright (c) 2020 Sebastian Panknin // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // define("translate", [], function() { // return { // coordinates: "coordinates", // not_found: "No result for your search found.", // no_position: "No location for this plant available", // missing_indicator: "–" // }; // }); define("translate", [], function() { return { coordinates: "Koordinaten", not_found: "Keine Pflanzen für diese Suche gefunden.", no_position: "Kein Standort für diese Pflanze verzeichnet.", missing_indicator: "–" }; }); define("display", ["map", "searchList", "data", "hoverGroup", "geoPos", "translate"], function(map, searchList, data, hover, geoPos, translate) { const resultsId = "#results"; const limitWarning = "#limitwarning"; const markerLimit = 24; const searchLimit = 24; const setNumbers = function(results) { results.filter( e => e.positions.length > 0 ).forEach( (e, i) => e.number = (i < markerLimit) ? i + 1 : undefined ); }; const displayLimitWarningIf = function(display) { const warningDivClasses = document.querySelector(limitWarning).classList; if(display) { warningDivClasses.remove("hidden"); } else { warningDivClasses.add("hidden"); } }; const searchFor = function(searchWord) { const unlimitedResults = searchList.filter(data.fullList, searchWord); const results = unlimitedResults.slice(0, searchLimit); displayLimitWarningIf(unlimitedResults.length !== results.length); setNumbers(results); searchList.renderResultTable(resultsId, results); map.setMarkers(results); results.forEach(r => { if(r.number) { hover.group(".plant-on-map-" + r.number, _ => map.highlight(r.number), _ => map.fade(r.number)); } }); }; const search = function() { let searchWord = document.querySelector(".filter").value; searchFor(searchWord); }; const searchOnEnter = function(e) { if (e.keyCode === 13 || e.keyCode === 10) { search(); } }; const activateSearch = function() { document.querySelector("#loadmessage").setAttribute("class", "hidden"); document.querySelector(".filter").disabled = false; document.querySelector(".search-button").disabled = false; }; const loadingDone = function(promise) { activateSearch(); let searchWord = decodeURI(location.search).replace(/^\?/, ""); if (searchWord) { document.querySelector(".filter").value = searchWord; searchFor(searchWord); } return promise; }; const hrefFromHiddenLink = function(identifier) { return }; const getDataSrc = function() { return document.querySelector("#datasrc a").href; }; const getMapSrc = function() { return document.querySelector("#mapsrc img").src; }; const getPointerImage = function() { return document.querySelector("#iconspot img").src; }; const getPointerFocusImage = function() { return document.querySelector("#iconspotfocus img").src; }; const init = function() { data.init( getDataSrc() ).then( loadingDone ).catch(e => { console.log("Error while loading data:", e); }); map.init( "map", getMapSrc(), { x: 1540, y: 1000, center: {lat: 51.5381715, long: 9.9374475}, pointerImage: getPointerImage(), pointerFocusImage: getPointerFocusImage() } ); const markCurrentPos = function(long, lat) { const valid = x => (x >= - 180) && (x <= 180) if (valid(long) && valid(lat)) { map.addMarker(long, lat, "X") } } geoPos.monitor(markCurrentPos) searchList.init( resultsId, translate, [] ); document.addEventListener("keydown", searchOnEnter); }; return { init: init, searchEvent: search }; }); define("geoPos", [], function() { const monitor = function (handler) { const runner = function(position) { handler(position.coords.longitude, position.coords.latitude) } const error = function() { console.log('Unable to retrieve your location') } const installMonitoring = function(position) { handler(position) setInterval(() => navigator.geolocation.getCurrentPosition(runner, error), 1000) } if (!navigator.geolocation) { console.log('Geolocation is not supported by your browser') } else { navigator.geolocation.getCurrentPosition(installMonitoring, error) } } return { monitor: monitor } }) define("csv", [], function(){ const escaping = function(chr) { const lookup = { "n": "\n", "r": "\r", "t": "\t", '"': '"', '\\': '\\', ',': ',' }; return lookup[chr] || "\\" + chr; }; const charAction = function(state, chr) { if (state.esc) { state.esc = false; state.cells[state.cells.length - 1] = state.cells[state.cells.length - 1] + escaping(chr); } else if (!state.ecs && chr === '\\') { state.ecs = true; } else if (!state.esc && chr === '"') { state.quote = !state.quote; } else if (!state.quote && !state.esc && chr === ",") { state.cells.push(""); } else { state.cells[state.cells.length - 1] = state.cells[state.cells.length - 1] + chr; } return state; }; const parseLine = function(line) { const chars = line.split(""); return chars.reduce(charAction, {cells: [""], quote: false, esc: false}).cells; }; const objectify = function(keys, values) { return keys.reduce(function (obj, key, i) { obj[key] = values[i]; return obj; }, {}); }; const parse = function(text) { const cellData = text.split(/\r?\n/).map(parseLine); const header = cellData[0]; return cellData.slice(1).map(cs => objectify(header, cs)); }; return {parse: parse}; }); define("data", ["csv"], function(csv) { const fullList = []; const debug = {}; const isDisplayable = function(row) { return row["vorhanden"] === "+" && row["Name"] !== ""; }; const positions = function(row) { const positionKeys = Object.keys(row).filter(key => key.match(/position.*/) && row[key]); const positionPairs = positionKeys.map(key => row[key].replace(/\((.*)\)/, "$1").split(",")); return positionPairs.map(function(p) { return {long: parseFloat(p[1]), lat: parseFloat(p[0])}; }); }; const toDisplayData = function(row) { return { name: row["Name"], trivial: row["Volksname"], synonym: row["Synonym"], family: row["Familie"], positions: positions(row)}; }; const parseData = function(text) { let csvData = csv.parse(text); debug.csv = csvData; return csvData.filter(isDisplayable).map(toDisplayData); }; const fetchData = function(url) { return fetch( url ).then( response => response.text() ).then( data => { let list = parseData(data); Array.prototype.push.apply(fullList, list); debug.out = list; } ); }; return { init: fetchData, fullList: fullList, debug: debug }; }); define("hoverGroup", [], function() { const forEachOfGroup = function(groupSelector, action) { document.querySelectorAll(groupSelector).forEach(action); }; const highlightClass = "highlight"; const addHighlight = function(el) { el.classList.add(highlightClass); }; const removeHighlight = function(el) { el.classList.remove(highlightClass); }; const group = function(groupSelector, onEnter, onLeave) { forEachOfGroup( groupSelector, el => { el.addEventListener("mouseenter", e => { forEachOfGroup(groupSelector, addHighlight) if(onEnter) { onEnter(e); } }); el.addEventListener("mouseleave", e => { forEachOfGroup(groupSelector, removeHighlight) if(onLeave) { onLeave(e); } }); } ) }; return { group: group }; }); define("htmlTools", [], function(){ const svgNs = "http://www.w3.org/2000/svg"; const ele = function(tag, ns) { return function(attributes, children) { let node = ns ? document.createElementNS(ns, tag) : document.createElement(tag); Object.keys(attributes).forEach(function(key){ node.setAttribute(key, attributes[key]); }); children.filter(c => c !== "none").forEach(function(child){ node.appendChild(child); }); return node; }; } const textNode = function(text) { return document.createTextNode(text); } const clearContainer = function(target) { let container = document.querySelector(target); while(container.firstChild) { container.removeChild(container.firstChild); } }; const render = function(target, node) { document.querySelector(target).appendChild(node); }; return { g: ele("g", svgNs), image: ele("image", svgNs), line: ele("line", svgNs), circle: ele("circle", svgNs), none: "none", a: ele("a"), span: ele("span"), svg: ele("svg", svgNs), table: ele("table"), td: ele("td"), text: ele("text", svgNs), textNode: textNode, tr: ele("tr"), clearContainer: clearContainer, render: render }; }); define("searchList", ["htmlTools"], function(h) { let messages = { coordinates: "COORDINTATES", not_found: "NOT_FOUND", no_position: "NO_POSITION", missing_indicator: "—" }; const filter = function(data, str) { prepFilter = s => s.toLowerCase().replace(/[-[\]{}()+.,\\^$|#\s]/g, '\\$&').replace(/\*/g, ".*").replace(/\?/g, ".").replace(/\s+$/, "") return data.filter( el => ( el.name + " " + el.trivial // + " " + el.synonym + " " + el.family // Synonym and Family are not desired criteria ).toLowerCase().match(prepFilter(str)) && el.positions.length > 0 ); }; const renderResultTable = function(target, data) { const mailto = function(to, additions) { const parameters = Object.keys(additions).map(k => encodeURIComponent(k) + "=" + encodeURIComponent(additions[k])).join("&") return "mailto:" + encodeURI(to) + "?" + parameters } h.clearContainer(target); h.render(target, data.length ? h.table( {"class": "result-table"}, data.map(e => h.tr( e.number ? {"class": "plant-on-map-" + e.number}: {}, [ h.td( {"class": "map-legend"}, [e.number ? h.span( { "class": "map-symbol map-symbol-number-" + e.number, title: messages.coordinates + ": " + e.positions.map(p => p.lat + "/" + p.long).join(", ") }, [h.textNode(e.number)] ) : h.span( { "class": " coordinates-missing", title: messages.no_position }, [h.textNode(messages.missing_indicator)]) ] ), h.td( {"class": "plant"}, [ h.span({"class": "plant-scientific-name"}, [h.textNode(e.name)]), (e.trivial) ? h.span( {"class": "plant-additional-data"}, [ e.trivial ? h.span({"class": "plant-trivial-name"}, [h.textNode(e.trivial)]) : h.none//, //e.synonym ? h.span({"class": "plant-synonym"}, [h.textNode(e.synonym)]) : h.none, //e.family ? h.span({"class": "plant-family"}, [h.textNode(e.family)]) : h.none ] ) : h.none ] ) ] ))) : h.span({"class": "not-found"}, [h.textNode(messages.not_found)]) ); }; const init = function(target, msg, data) { Object.keys(msg).forEach(k => messages[k] = msg[k]); }; return { init: init, filter: filter, renderResultTable: renderResultTable }; });