Skip to content

Commit

Permalink
Merge pull request #19 from matafokka/development
Browse files Browse the repository at this point in the history
Beta 0.2.0
  • Loading branch information
matafokka authored Apr 17, 2022
2 parents e109b4c + b1d01d0 commit a6de08e
Show file tree
Hide file tree
Showing 25 changed files with 708 additions and 127 deletions.
2 changes: 1 addition & 1 deletion PWAServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const cacheName = "SynthFlight", matchOptions = {ignoreSearch: true};
// Cache project files. More files are added in build.js. Preserve the comment inside of array!
// The whole thing works like auto update.
caches.open(cacheName).then(cache => cache.addAll([
"./", "PWAServiceWorker.js", "css/styles.css", "main.js", /** to_cache_list */
"/", "/PWAServiceWorker.js", "/css/styles.css", "/main.js", /** to_cache_list */
]));

// On resource fetch, try to load it from cache or pass request to fetch()
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ There are numerous ways to set up SynthFlight, listed from most to least preferr
1. Extract the downloaded archive wherever you want.
1. Navigate to the extracted folder, open it and run `SynthFlight` executable file.

***Warning 1:** only browser and Windows x64 builds has been tested so far.*
***Warning 1:** only Windows x64 builds has been tested so far.*

***Warning 2:** macOS builds are not signed, thus require disabling the Gatekeeper or something.*

Expand All @@ -48,9 +48,12 @@ One of:

**Problems with outdated browsers:**

1. Can't read files. Chromium-based browsers have a "feature" that prevents FileReader from reading local files. **How to solve:** run browser with `--allow-file-access-from-files` flag or [host SynthFlight](#hosting) on a custom server.
1. Can't read files. Chromium-based browsers have a "feature" that prevents `FileReader` from reading local files. **How to solve:** run browser with `--allow-file-access-from-files` flag or [host SynthFlight](#hosting) on a custom server.
1. Can't save files. **How to solve:** update your browser.
1. App page doesn't load (browser can't verify SSL certificate). **How to solve:** configure your browser or [host SynthFlight](#hosting) on a custom server using protocols and/or certificates that your browser supports.
1. App page doesn't load in legacy browsers (browser can't verify SSL certificate). **How to solve:** enable TLS 1.3 support in your browser or [host SynthFlight](#hosting) on a custom server using protocols and/or certificates that your browser supports.
1. OSM search doesn't work in browsers that doesn't support TLS >= 1.2. **How to solve:** update your browser. **For IE9**, you need to serve SynthFlight over HTTPS and make sure your users have TLS 1.2 support enabled.

Of course, requirements for TLS might change in future with the new TLS versions coming out and GitHub and OSM changing their policies. You can't prevent this from happening, the only thing you can do is using an evergreen browser.


## For desktop builds
Expand Down
26 changes: 26 additions & 0 deletions SearchControl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Creates a button that should open search window, i.e. should be passed to the {@link L.ALS.SearchWindow} constructor.
* @returns {HTMLElement}
*/
L.ALS._createSearchButton = function () {
let button = document.createElement("i");
button.className = "ri ri-search-line";
L.ALS.Locales.localizeElement(button, "searchButtonTitle", "title");
return button;
}

/**
* A control that opens search window. To get HTMLElement, use {@link L.SearchControl#getContainer}.
* @class
* @extends L.Control
*/
L.SearchControl = L.Control.extend(/** @lends L.SearchControl.prototype */{
initialize: function (options) {
L.Control.prototype.initialize.call(this, options);
this._button = L.ALS._createSearchButton();
},

onAdd: function (map) {
return this._button;
}
});
233 changes: 233 additions & 0 deletions SearchWindow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
const MiniSearch = require("minisearch");
const debounce = require("debounce");

/**
* A window that searches Geometry Layers and OSM
*
* @class
* @extends L.ALS.WidgetableWindow
*/
L.ALS.SearchWindow = L.ALS.WidgetableWindow.extend(/** @lends L.ALS.SearchWindow.prototype */{
initialize: function (map, button = undefined) {
L.ALS.WidgetableWindow.prototype.initialize.call(this, button);

/**
* {@link L.ALS.SynthGeometryLayer}s to search in
* @private
*/
this._docs = {}
this.updateIndex();

// Add window to the document
document.body.appendChild(this.windowContainer);
this.container.classList.add("search-window-container");

// Create search input and insert it before the content
let searchInput = document.createElement("input");
searchInput.className = "search-input";
searchInput.type = "text";
L.ALS.Locales.localizeElement(searchInput, "searchPlaceholder", "placeholder");
this.window.insertBefore(searchInput, this.container);

let layersContainer = document.createElement("div"),
osmContainer = document.createElement("div");
this.container.appendChild(layersContainer);
this.container.appendChild(osmContainer);

this.window.classList.add("search-window");

let closeButton = this.addCloseButton("close", "searchCloseButton"),

// When search result is clicked, fly to the target's bbox
onClick = (e) => {
L.ALS.Helpers.dispatchEvent(closeButton.input, "click");
let bbox = e.target.bbox;
map.flyToBounds(
L.latLngBounds(L.latLng(bbox[1], bbox[0]), L.latLng(bbox[3], bbox[2])), {
animate: true,
duration: 1
});
},

// When results are found, adds them to the window
addResults = (results, isOSM = true) => {
let title = document.createElement("div"), noResults = results.length === 0, container;
title.className = "als-window-sidebar-title";

if (isOSM) {
container = osmContainer;
L.ALS.Locales.localizeElement(title, noResults ? "searchNoOSMResults" : "searchOSMResults");
}
else {
container = layersContainer;
L.ALS.Locales.localizeElement(title, noResults ? "searchNoLayersResults" : "searchLayersResults");
}

container.appendChild(title);

for (let result of results) {
let elem = document.createElement("div");
elem.className = "als-button-base search-result";

if (isOSM)
elem.textContent = result.properties.display_name; // Use name returned by the API for OSM results
else {
// Build name from matching terms for layers
let name = "";
for (let term in result.match) {
if (!result.match.hasOwnProperty(term))
continue;
let key = result.match[term],
newName = name + `<b>${key}</b>: ${result.properties[key]}; `;

// Name should contain at least one key and not be too long. The used way of cropping is the
// best compromise for long keys and long values, I think.
if (name !== "" && newName > 500)
break;

name += `<b>${key}</b>: ${result.properties[key]}; `;
}

name = name.substring(0, name.length - 2); // Remove last divider
L.ALS.Helpers.HTMLToElement(name, elem);
}

elem.bbox = result.bbox;
elem.addEventListener("click", onClick);
container.appendChild(elem);
}
},

// If got an error when fetching data from the Nominatim, adds an error message
displayError = (errorText, parseAsHTML = false) => {
let label = new L.ALS.Widgets.SimpleLabel("id", parseAsHTML ? " " : errorText, "justify", "error");

if (parseAsHTML)
L.ALS.Helpers.HTMLToElement(errorText, label.input);

osmContainer.appendChild(label.container);
},

// Clears all children from the given container
clearContainer = (container) => {
while (container.firstChild)
container.removeChild(container.firstChild);
},

// Search function, called on input
search = () => {
// Search layers
clearContainer(layersContainer);
addResults(this._search.search(searchInput.value, {fuzzy: true}), false);

// Search OSM through Nominatim API
// Nominatim redirects all requests to the HTTPS with TLS 1.2. Legacy browsers doesn't support it,
// but we'll still do requests through XHR/XDR in case there will be an API that supports
// non-secure HTTP. If this'll ever happen, we'll be able to quickly replace Nominatim with it.

let request = L.ALS.Helpers.isIElte9 ? new XDomainRequest() : new XMLHttpRequest();

// "progress" event should always be handled in IE9
if (L.ALS.Helpers.isIElte9)
request.onprogress = (e) => console.log(e);

// Handle connection loss
request.onerror = () => {
clearContainer(osmContainer);
displayError("searchCantConnect");
};

request.onload = (e) => {
if (request.readyState !== 4)
return;

clearContainer(osmContainer);

// If server responded, try to parse GeoJSON
if (request.status === 200) {
try {
let json = JSON.parse(request.responseText);
addResults(json.features);
} catch (e) {
// Handle invalid GeoJSON
console.log(e);
displayError("searchInvalidJson");
}
return;
}

// Handle bad responses

let responseText = request.responseText.trim(),
errText = request.responseText.startsWith(request.status) ? responseText : `${request.status}: ${responseText}`;

displayError(
// Error, OSM server responded with the following message: "message_text". Please, try opening
`${L.ALS.locale.searchBadResponse1}: "${errText}". ${L.ALS.locale.searchBadResponse2} ` +
// OSM search in browser
`<a href="https://nominatim.openstreetmap.org" target="_blank">${L.ALS.locale.searchBadResponse3}</a>. ` +
// If it doesn't work, OSM search is temporarily unavailable. Otherwise, please, create an issue at
L.ALS.locale.searchBadResponse4 +
// SynthFlight repository
` <a href="https://github.com/matafokka/SynthFlight/issues" target="_blank">${L.ALS.locale.searchBadResponse5}</a>.`
, true);
}

// Get query and sanitize it. replace() removes the UTF surrogates.
let query = encodeURIComponent(searchInput.value.replace(/[\ud800-\udfff]/g, ""));
request.open("GET", `https://nominatim.openstreetmap.org/search?q=${query}&format=geojson`, true);

try {
request.send();
} catch (e) {
console.log(e);
}
}

searchInput.addEventListener("input", debounce(search, 200));
},

/**
* Adds MiniSearch document to search in
* @param layerId {string} Layer ID
* @param docs {Object[]} Documents to add
* @param fields {string[]} Fields to search on
*/
addToSearch: function (layerId, docs, fields) {
this._docs[layerId] = {docs, fields};
this.updateIndex();
},

/**
* Removes doc added by layerId from the search
* @param layerId {string} Layer ID
*/
removeFromSearch: function (layerId) {
delete this._docs[layerId];
this.updateIndex();
},

/**
* Updates search index
*/
updateIndex: function () {
let docs = [], fields = [];

for (let id in this._docs) {
if (!this._docs.hasOwnProperty(id))
continue;

let layerData = this._docs[id];
docs.push(...layerData.docs);
fields.push(...layerData.fields);
}

this._search = new MiniSearch({
fields,
storeFields: ["id", "bbox", "properties"],
idField: "_miniSearchId"
});

this._search.addAll(docs);
}
})
46 changes: 37 additions & 9 deletions SynthBase/SynthBaseLayer.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require("./SynthBaseSettings.js");
const turfHelpers = require("@turf/helpers");
const MathTools = require("../MathTools.js");
const debounce = require("debounce");

/**
* @typedef {Object} PathData
Expand Down Expand Up @@ -36,8 +37,19 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot
*/
dashedLine: "4 4",

isAfterDeserialization: false,

init: function (settings, pathGroup1, connectionsGroup1, colorLabel1, path1AdditionalLayers = [], pathGroup2 = undefined, connectionsGroup2 = undefined, colorLabel2 = undefined, path2AdditionalLayers = []) {

/**
* {@link L.ALS.Layer#writeToHistory} but debounced for use in repeated calls
* @type {function()}
*/
this.writeToHistoryDebounced = debounce(() => {
if (!this.isAfterDeserialization)
this.writeToHistory()
}, 300);

/**
* Settings passed from ALS
* @type {Object}
Expand Down Expand Up @@ -111,7 +123,7 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot
this.toUpdateThickness.push(path.pathGroup, path.connectionsGroup);
}

this.serializationIgnoreList.push("_airportMarker", "toUpdateThickness");
this.serializationIgnoreList.push("_airportMarker", "toUpdateThickness", "writeToHistoryDebounced");

/**
* Properties to copy to GeoJSON when exporting
Expand Down Expand Up @@ -245,22 +257,38 @@ L.ALS.SynthBaseLayer = L.ALS.Layer.extend(/** @lends L.ALS.SynthBaseLayer.protot
}
},


setAirportLatLng: function () {
this._airportMarker.setLatLng([
this.getWidgetById("airportLat").getValue(),
this.getWidgetById("airportLng").getValue()
]);
let latWidget = this.getWidgetById("airportLat"), lngWidget = this.getWidgetById("airportLng"),
fixedLatLng = this._limitAirportPos(latWidget.getValue(), lngWidget.getValue());

latWidget.setValue(fixedLatLng.lat);
lngWidget.setValue(fixedLatLng.lng);
this._airportMarker.setLatLng(fixedLatLng);
this.connectToAirport();
},

onMarkerDrag: function () {
let latLng = this._airportMarker.getLatLng();
this.getWidgetById("airportLat").setValue(latLng.lat.toFixed(5));
this.getWidgetById("airportLng").setValue(latLng.lng.toFixed(5));
let latLng = this._airportMarker.getLatLng(),
fixedLatLng = this._limitAirportPos(latLng.lat, latLng.lng);
this._airportMarker.setLatLng(fixedLatLng);
this.getWidgetById("airportLat").setValue(fixedLatLng.lat.toFixed(5));
this.getWidgetById("airportLng").setValue(fixedLatLng.lng.toFixed(5));
this.connectToAirport();
},

_limitAirportPos: function (lat, lng) {
if (lat > 85)
lat = 85;
if (lat < -85)
lat = -85;
if (lng > 180)
lng = 180;
if (lng < -180)
lng = -180;

return L.latLng(lat, lng);
},

onNameChange: function () {
let popup = document.createElement("div");
L.ALS.Locales.localizeElement(popup, "airportForLayer", "innerText");
Expand Down
1 change: 1 addition & 0 deletions SynthBase/calculateParameters.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ L.ALS.SynthBaseLayer.prototype.calculateParameters = function () {
this.getWidgetById(name).setValue(value);
}

this.writeToHistoryDebounced();
}
2 changes: 1 addition & 1 deletion SynthBase/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ L.ALS.SynthBaseLayer.prototype.enableDraw = function (drawControls, drawingGroup
this.addEventListenerTo(this.map, "draw:created", "onDraw");
this.addEventListenerTo(this.map, "draw:drawstart draw:editstart draw:deletestart", "onEditStart");
this.addEventListenerTo(this.map, "draw:drawstop draw:editstop draw:deletestop", "onEditEnd");
this.addControl(this.drawControl, "top", "topleft");
this.addControl(this.drawControl, "top", "follow-menu");
}

L.ALS.SynthBaseLayer.prototype.onDraw = function (e) {
Expand Down
Loading

0 comments on commit a6de08e

Please sign in to comment.