Add QR-code to all popups, add direction indicator to popup and visual feedback, make reviews accessible to screenreaders (both to read them and to make them)
This commit is contained in:
parent
5567869bb4
commit
bfd818cb38
33 changed files with 415 additions and 98 deletions
|
@ -40,6 +40,30 @@
|
|||
"centroid"
|
||||
],
|
||||
"anchor": "center"
|
||||
},
|
||||
{
|
||||
"marker": [
|
||||
{
|
||||
"color": "--catch-detail-color",
|
||||
"icon": "direction"
|
||||
}
|
||||
],
|
||||
"iconSize": {
|
||||
"render": "0,0",
|
||||
"mappings": [
|
||||
{
|
||||
"if": "alpha~*",
|
||||
"then": "40,40"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pitchAlignment": "map",
|
||||
"rotation": "{alpha}deg",
|
||||
"location": [
|
||||
"point",
|
||||
"centroid"
|
||||
],
|
||||
"anchor": "center"
|
||||
}
|
||||
],
|
||||
"lineRendering": []
|
||||
|
|
|
@ -302,6 +302,14 @@
|
|||
"condition": "_favourite=yes",
|
||||
"icon": "circle:white;heart:red",
|
||||
"metacondition": "__showTimeSensitiveIcons!=no"
|
||||
},
|
||||
{
|
||||
"id": "direction",
|
||||
"labels": [
|
||||
"defaults",
|
||||
"in_favourite"
|
||||
],
|
||||
"render": "{direction_indicator()}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -366,7 +366,7 @@
|
|||
},
|
||||
"freeform": {
|
||||
"key": "min_age",
|
||||
"type": "pnat"
|
||||
"type": "nat"
|
||||
},
|
||||
"id": "playground-min_age"
|
||||
},
|
||||
|
|
|
@ -399,12 +399,34 @@
|
|||
"useSearchForMore": "Use the search function to search within {total} more values…",
|
||||
"visualFeedback": {
|
||||
"closestFeaturesAre": "{n} features within viewport.",
|
||||
"directionsAbsolute": {
|
||||
"E": "east",
|
||||
"N": "north",
|
||||
"NE": "northeast",
|
||||
"NW": "northwest",
|
||||
"S": "south",
|
||||
"SE": "southeast",
|
||||
"SW": "southwest",
|
||||
"W": "west"
|
||||
},
|
||||
"directionsRelative": {
|
||||
"behind": "on your back",
|
||||
"left": "left",
|
||||
"right": "right",
|
||||
"sharp_left": "sharply left",
|
||||
"sharp_right": "sharply right",
|
||||
"slight_left": "slightly left",
|
||||
"slight_right": "slightly right",
|
||||
"straight": "straight ahead"
|
||||
},
|
||||
"east": "Moving east",
|
||||
"fromGps": "{distance} {direction} of your location",
|
||||
"fromMapCenter": "{distance} {direction} of the map center",
|
||||
"in": "Zooming in to level {z}",
|
||||
"islocked": "View locked to your GPS-location, moving disabled. Press the geolocation button to unlock.",
|
||||
"locked": "View is now locked to your GPS-location, moving disabled.",
|
||||
"navigation": "Use arrow keys to move the map, press space to select the closest feature. Press a number to select locations further away.",
|
||||
"noCloseFeatures": "No features in view",
|
||||
"noCloseFeatures": "No features in view.",
|
||||
"north": "Moving north",
|
||||
"oneFeatureInView": "One feature within viewport.",
|
||||
"out": "Zooming out to level {z}",
|
||||
|
@ -636,12 +658,15 @@
|
|||
"reviews": {
|
||||
"affiliated_reviewer_warning": "(Affiliated review)",
|
||||
"attribution": "Reviews are powered by <a href='https://mangrove.reviews/' target='_blank'>Mangrove Reviews</a> and are available under <a href='https://mangrove.reviews/terms#8-licensing-of-content' target='_blank'>CC-BY 4.0</a>.",
|
||||
"averageRating": "Average rating of {n} stars",
|
||||
"i_am_affiliated": "I am affiliated with this object",
|
||||
"i_am_affiliated_explanation": "Check if you are an owner, creator, employee, …",
|
||||
"name_required": "A name is required in order to display and create reviews",
|
||||
"no_reviews_yet": "There are no reviews yet. Be the first to write one and help open data and the business!",
|
||||
"question": "How would you rate {title()}?",
|
||||
"question_opinion": "How was your experience?",
|
||||
"rate": "Rate {n} stars",
|
||||
"rated": "Rated {n} stars",
|
||||
"reviewing_as": "Reviewing as {nickname}",
|
||||
"reviewing_as_anonymous": "Reviewing as anonymous",
|
||||
"save": "Save",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "mapcomplete",
|
||||
"version": "0.36.7",
|
||||
"version": "0.36.8",
|
||||
"repository": "https://github.com/pietervdvn/MapComplete",
|
||||
"description": "A small website to edit OSM easily",
|
||||
"bugs": "https://github.com/pietervdvn/MapComplete/issues",
|
||||
|
|
|
@ -1347,6 +1347,10 @@ video {
|
|||
appearance: none;
|
||||
}
|
||||
|
||||
.grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
@ -1450,14 +1454,26 @@ video {
|
|||
row-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gap-x-1 {
|
||||
-webkit-column-gap: 0.25rem;
|
||||
column-gap: 0.25rem;
|
||||
}
|
||||
|
||||
.gap-x-2 {
|
||||
-webkit-column-gap: 0.5rem;
|
||||
column-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.gap-x-1 {
|
||||
-webkit-column-gap: 0.25rem;
|
||||
column-gap: 0.25rem;
|
||||
.space-x-0\.5 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.125rem * var(--tw-space-x-reverse));
|
||||
margin-left: calc(0.125rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-x-0 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0px * var(--tw-space-x-reverse));
|
||||
margin-left: calc(0px * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-x-1 > :not([hidden]) ~ :not([hidden]) {
|
||||
|
@ -1466,6 +1482,18 @@ video {
|
|||
margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));
|
||||
}
|
||||
|
||||
.space-y-0\.5 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0.125rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(0.125rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-y-0 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(0px * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-x-reverse: 0;
|
||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||
|
@ -1554,6 +1582,10 @@ video {
|
|||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.break-words {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
@ -1695,11 +1727,6 @@ video {
|
|||
border-color: rgb(219 234 254 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-red-500 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(239 68 68 / var(--tw-border-opacity));
|
||||
}
|
||||
|
||||
.border-gray-300 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgb(209 213 219 / var(--tw-border-opacity));
|
||||
|
|
|
@ -10,6 +10,7 @@ import { GeoOperations } from "../GeoOperations"
|
|||
import { OsmTags } from "../../Models/OsmFeature"
|
||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"
|
||||
import { MapProperties } from "../../Models/MapProperties"
|
||||
import { Orientation } from "../../Sensors/Orientation"
|
||||
|
||||
/**
|
||||
* The geolocation-handler takes a map-location and a geolocation state.
|
||||
|
@ -128,10 +129,10 @@ export default class GeoLocationHandler {
|
|||
}
|
||||
|
||||
// We check that the GPS location is not out of bounds
|
||||
const bounds = this.mapProperties.maxbounds.data
|
||||
const bounds: BBox = this.mapProperties.maxbounds.data
|
||||
if (bounds !== undefined) {
|
||||
// B is an array with our lock-location
|
||||
const inRange = new BBox(bounds).contains([newLocation.longitude, newLocation.latitude])
|
||||
const inRange = bounds.contains([newLocation.longitude, newLocation.latitude])
|
||||
if (!inRange) {
|
||||
return
|
||||
}
|
||||
|
@ -167,6 +168,9 @@ export default class GeoLocationHandler {
|
|||
altitude: location.altitude,
|
||||
altitudeAccuracy: location.altitudeAccuracy,
|
||||
heading: location.heading,
|
||||
alpha: Orientation.singleton.gotMeasurement.data
|
||||
? "" + Orientation.singleton.alpha.data
|
||||
: undefined,
|
||||
}
|
||||
i++
|
||||
|
||||
|
|
|
@ -55,8 +55,9 @@ export default class FeatureReviews {
|
|||
private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {}
|
||||
public readonly subjectUri: Store<string>
|
||||
public readonly average: Store<number | null>
|
||||
private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
|
||||
new UIEventSource([])
|
||||
private readonly _reviews: UIEventSource<
|
||||
(Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> })[]
|
||||
> = new UIEventSource([])
|
||||
public readonly reviews: Store<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
|
||||
this._reviews
|
||||
private readonly _lat: number
|
||||
|
@ -176,11 +177,15 @@ export default class FeatureReviews {
|
|||
...review,
|
||||
}
|
||||
const keypair: CryptoKeyPair = this._identity.keypair.data
|
||||
console.log(r)
|
||||
const jwt = await MangroveReviews.signReview(keypair, r)
|
||||
console.log("Signed:", jwt)
|
||||
const kid = await MangroveReviews.publicToPem(keypair.publicKey)
|
||||
await MangroveReviews.submitReview(jwt)
|
||||
this._reviews.data.push({ ...r, madeByLoggedInUser: new ImmutableStore(true) })
|
||||
this._reviews.data.push({
|
||||
...r,
|
||||
kid,
|
||||
signature: jwt,
|
||||
madeByLoggedInUser: new ImmutableStore(true),
|
||||
})
|
||||
this._reviews.ping()
|
||||
}
|
||||
|
||||
|
@ -189,7 +194,7 @@ export default class FeatureReviews {
|
|||
* @param reviews
|
||||
* @private
|
||||
*/
|
||||
private addReviews(reviews: { payload: Review; kid: string }[]) {
|
||||
private addReviews(reviews: { payload: Review; kid: string; signature: string }[]) {
|
||||
const self = this
|
||||
const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion))
|
||||
|
||||
|
@ -199,7 +204,6 @@ export default class FeatureReviews {
|
|||
|
||||
try {
|
||||
const url = new URL(review.sub)
|
||||
console.log("URL is", url)
|
||||
if (url.protocol === "geo:") {
|
||||
const coordinate = <[number, number]>(
|
||||
url.pathname.split(",").map((n) => Number(n))
|
||||
|
@ -222,6 +226,8 @@ export default class FeatureReviews {
|
|||
}
|
||||
self._reviews.data.push({
|
||||
...review,
|
||||
kid: reviewData.kid,
|
||||
signature: reviewData.signature,
|
||||
madeByLoggedInUser: this._identity.key_id.map((user_key_id) => {
|
||||
return reviewData.kid === user_key_id
|
||||
}),
|
||||
|
|
|
@ -99,6 +99,7 @@ export default class Constants {
|
|||
* In seconds
|
||||
*/
|
||||
static zoomToLocationTimeout = 15
|
||||
public static readonly viewportCenterCloseToGpsCutoff: number = 20
|
||||
private static readonly config = (() => {
|
||||
const defaultConfig = packagefile.config
|
||||
return { ...defaultConfig, ...extraconfig }
|
||||
|
|
|
@ -576,6 +576,7 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
|||
"last_edit",
|
||||
"favourite_state",
|
||||
"all_tags",
|
||||
"qr_code",
|
||||
]
|
||||
private readonly _desugaring: DesugaringContext
|
||||
|
||||
|
@ -657,6 +658,13 @@ export class AddEditingElements extends DesugaringStep<LayerConfigJson> {
|
|||
})
|
||||
}
|
||||
|
||||
if (!usedSpecialFunctions.has("qr_code")) {
|
||||
json.tagRenderings.push({
|
||||
id: "qr_code",
|
||||
render: { "*": "{qr_code()}" },
|
||||
})
|
||||
}
|
||||
|
||||
if (!usedSpecialFunctions.has("all_tags")) {
|
||||
const trc: QuestionableTagRenderingConfigJson = {
|
||||
id: "all-tags",
|
||||
|
|
|
@ -631,6 +631,10 @@ export default class TagRenderingConfig {
|
|||
* , "testcase")
|
||||
* config.constructChangeSpecification(undefined, undefined, [false, true, false], {amenity: "public_bookcase"}) // => new And([new Tag("books","adults")])
|
||||
*
|
||||
* const config = new TagRenderingConfig({"id":"capacity", "render": "Fits {capcity} books",freeform: {"key":"capacity",type:"pnat"} })
|
||||
* config.constructChangeSpecification("", undefined, undefined, {}) // => undefined
|
||||
* config.constructChangeSpecification("5", undefined, undefined, {}).optimize() // => new Tag("capacity", "5")
|
||||
*
|
||||
* @param freeformValue The freeform value which will be applied as 'freeform.key'. Ignored if 'freeform.key' is not set
|
||||
*
|
||||
* @param singleSelectedMapping (Only used if multiAnswer == false): the single mapping to apply. Use (mappings.length) for the freeform
|
||||
|
|
|
@ -49,7 +49,7 @@ export class Orientation {
|
|||
if (rotateAlpha) {
|
||||
this._animateFakeMeasurements = true
|
||||
Stores.Chronic(25).addCallback((date) => {
|
||||
this.alpha.setData((date.getTime() / 100) % 360)
|
||||
this.alpha.setData((date.getTime() / 50) % 360)
|
||||
if (!this._animateFakeMeasurements) {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -8,20 +8,25 @@
|
|||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import Compass_arrow from "../../assets/svg/Compass_arrow.svelte"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { Orientation } from "../../Sensors/Orientation"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Constants from "../../Models/Constants"
|
||||
import Locale from "../i18n/Locale"
|
||||
import { ariaLabelStore } from "../../Utils/ariaLabel"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export let state: ThemeViewState
|
||||
export let state: SpecialVisualizationState
|
||||
export let feature: Feature
|
||||
export let size = "w-8 h-8"
|
||||
|
||||
let fcenter = GeoOperations.centerpointCoordinates(feature)
|
||||
// Bearing and distance relative to the map center
|
||||
let bearingAndDist: Store<{ bearing: number; dist: number }> = state.mapProperties.location.map(
|
||||
(l) => {
|
||||
let mapCenter = [l.lon, l.lat]
|
||||
let bearing = Math.round(GeoOperations.bearing(fcenter, mapCenter))
|
||||
let bearing = Math.round(GeoOperations.bearing(mapCenter, fcenter))
|
||||
let dist = Math.round(GeoOperations.distanceBetween(fcenter, mapCenter))
|
||||
return { bearing, dist }
|
||||
},
|
||||
|
@ -29,19 +34,87 @@
|
|||
let bearingFromGps = state.geolocation.geolocationState.currentGPSLocation.mapD(coordinate => {
|
||||
return GeoOperations.bearing([coordinate.longitude, coordinate.latitude], fcenter)
|
||||
})
|
||||
let compass = Orientation.singleton.alpha.map(compass => compass ?? 0)
|
||||
export let size = "w-8 h-8"
|
||||
let compass = Orientation.singleton.alpha
|
||||
|
||||
let relativeDirections = Translations.t.general.visualFeedback.directionsRelative
|
||||
let absoluteDirections = Translations.t.general.visualFeedback.directionsAbsolute
|
||||
|
||||
let closeToCurrentLocation = state.geolocation.geolocationState.currentGPSLocation.map(gps => {
|
||||
if (!gps) {
|
||||
return false
|
||||
}
|
||||
let l = state.mapProperties.location.data
|
||||
let mapCenter = [l.lon, l.lat]
|
||||
const dist = GeoOperations.distanceBetween([gps.longitude, gps.latitude], mapCenter)
|
||||
return dist < Constants.viewportCenterCloseToGpsCutoff
|
||||
},
|
||||
[state.mapProperties.location],
|
||||
)
|
||||
let labelFromCenter: Store<string> = bearingAndDist.mapD(({ bearing, dist }) => {
|
||||
const distHuman = GeoOperations.distanceToHuman(dist)
|
||||
const lang = Locale.language.data
|
||||
const t = absoluteDirections[GeoOperations.bearingToHuman(bearing)]
|
||||
const mainTr = Translations.t.general.visualFeedback.fromMapCenter.Subs({
|
||||
distance: distHuman,
|
||||
direction: t.textFor(lang),
|
||||
})
|
||||
return mainTr.textFor(lang)
|
||||
}, [compass, Locale.language])
|
||||
|
||||
|
||||
// Bearing and distance relative to the map center
|
||||
let bearingAndDistGps: Store<{
|
||||
bearing: number;
|
||||
dist: number
|
||||
} | undefined> = state.geolocation.geolocationState.currentGPSLocation.mapD(
|
||||
({ longitude, latitude }) => {
|
||||
let gps = [longitude, latitude]
|
||||
let bearing = Math.round(GeoOperations.bearing(gps, fcenter))
|
||||
let dist = Math.round(GeoOperations.distanceBetween(fcenter, gps))
|
||||
return { bearing, dist }
|
||||
},
|
||||
)
|
||||
let labelFromGps: Store<string | undefined> = bearingAndDistGps.mapD(({ bearing, dist }) => {
|
||||
const distHuman = GeoOperations.distanceToHuman(dist)
|
||||
const lang = Locale.language.data
|
||||
let bearingHuman: string
|
||||
if (compass.data !== undefined) {
|
||||
console.log("compass:", compass.data)
|
||||
const bearingRelative = bearing - compass.data
|
||||
const t = relativeDirections[GeoOperations.bearingToHumanRelative(bearingRelative)]
|
||||
bearingHuman = t.textFor(lang)
|
||||
} else {
|
||||
bearingHuman = absoluteDirections[GeoOperations.bearingToHuman(bearing)].textFor(lang)
|
||||
}
|
||||
const mainTr = Translations.t.general.visualFeedback.fromGps.Subs({
|
||||
distance: distHuman,
|
||||
direction: bearingHuman,
|
||||
})
|
||||
return mainTr.textFor(lang)
|
||||
}, [compass, Locale.language])
|
||||
|
||||
let label = labelFromCenter.map(labelFromCenter => {
|
||||
if (labelFromGps.data !== undefined) {
|
||||
if(closeToCurrentLocation.data){
|
||||
return labelFromGps.data
|
||||
}
|
||||
return labelFromCenter + ", " + labelFromGps.data
|
||||
}
|
||||
return labelFromCenter
|
||||
}, [labelFromGps])
|
||||
function focusMap(){
|
||||
state.mapProperties.location.setData({ lon: fcenter[0], lat: fcenter[1] })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={twMerge("relative", size)}>
|
||||
<div class={twMerge("absolute top-0 left-0 flex items-center justify-center text-sm",size)}>
|
||||
{GeoOperations.distanceToHuman($bearingAndDist.dist)}
|
||||
<button class={twMerge("relative rounded-full soft", size)} use:ariaLabelStore={label} on:click={() => focusMap()}>
|
||||
<div class={twMerge("absolute top-0 left-0 flex items-center justify-center text-sm break-words",size)}>
|
||||
{GeoOperations.distanceToHuman($bearingAndDistGps.dist)}
|
||||
</div>
|
||||
{#if $bearingFromGps !== undefined}
|
||||
<div class={twMerge("absolute top-0 left-0 rounded-full border border-gray-500", size)}>
|
||||
<div class={twMerge("absolute top-0 left-0 rounded-full", size)}>
|
||||
<Compass_arrow class={size}
|
||||
style={`transform: rotate( calc( 45deg + ${$bearingFromGps - $compass}deg) );`} />
|
||||
style={`transform: rotate( calc( 45deg + ${$bearingFromGps - ($compass ?? 0)}deg) );`} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<span>{$bearingAndDist.bearing}° {GeoOperations.bearingToHuman($bearingAndDist.bearing)} {GeoOperations.bearingToHumanRelative($bearingAndDist.bearing - $compass)}</span>
|
||||
</button>
|
||||
|
|
|
@ -11,11 +11,12 @@
|
|||
export let cls: string = ""
|
||||
// Text for the current language
|
||||
let txt: Store<string | undefined> = t?.current
|
||||
$: {txt = t?.current}
|
||||
</script>
|
||||
|
||||
{#if $txt}
|
||||
<span class={cls}>
|
||||
<FromHtml src={$txt} />
|
||||
<WeblateLink context={t.context} />
|
||||
<WeblateLink context={t?.context} />
|
||||
</span>
|
||||
{/if}
|
||||
|
|
|
@ -4,12 +4,16 @@
|
|||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { Orientation } from "../../Sensors/Orientation"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Constants from "../../Models/Constants"
|
||||
|
||||
/**
|
||||
* Indicates how far away the viewport center is from the current user location
|
||||
*/
|
||||
export let state: ThemeViewState
|
||||
const t = Translations.t.general.visualFeedback
|
||||
const relativeDir = t.directionsRelative
|
||||
let map = state.mapProperties
|
||||
|
||||
let currentLocation = state.geolocation.geolocationState.currentGPSLocation
|
||||
|
@ -23,14 +27,29 @@
|
|||
const distanceInMeters = Math.round(GeoOperations.distanceBetween(gps, mapCenter))
|
||||
const distance = GeoOperations.distanceToHuman(distanceInMeters)
|
||||
const bearing = Math.round(GeoOperations.bearing(gps, mapCenter))
|
||||
return { distance, bearing, distanceInMeters }
|
||||
const bearingDirection = GeoOperations.bearingToHuman(bearing)
|
||||
return { distance, bearing, distanceInMeters, bearingDirection }
|
||||
}, [currentLocation])
|
||||
let hasCompass = Orientation.singleton.gotMeasurement
|
||||
let compass = Orientation.singleton.alpha
|
||||
let relativeBearing: Store<{distance: string, bearing: Translation}> =
|
||||
compass.mapD(compass => {
|
||||
const bearing: Translation = relativeDir[GeoOperations.bearingToHumanRelative(distanceToCurrentLocation.data.bearing - compass)]
|
||||
return {bearing, distance: distanceToCurrentLocation.data.distance}
|
||||
}, [distanceToCurrentLocation])
|
||||
let viewportCenterDetails = Translations.DynamicSubstitute(t.viewportCenterDetails, relativeBearing)
|
||||
let viewportCenterDetailsAbsolute = Translations.DynamicSubstitute(t.viewportCenterDetails, distanceToCurrentLocation.map(({distance, bearing}) => {
|
||||
return {distance, bearing: t.directionsAbsolute[GeoOperations.bearingToHuman(bearing)]}
|
||||
}))
|
||||
|
||||
</script>
|
||||
|
||||
{#if $currentLocation !== undefined}
|
||||
{#if $distanceToCurrentLocation.distanceInMeters < 20}
|
||||
{#if $distanceToCurrentLocation.distanceInMeters < Constants.viewportCenterCloseToGpsCutoff}
|
||||
<Tr t={t.viewportCenterCloseToGps} />
|
||||
{:else if $hasCompass}
|
||||
{$viewportCenterDetails}
|
||||
{:else}
|
||||
<Tr t={t.viewportCenterDetails.Subs($distanceToCurrentLocation)} />
|
||||
{$viewportCenterDetailsAbsolute}
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
@ -55,11 +55,10 @@
|
|||
|
||||
{#if currentLocation}
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
class="normal-background border-interactive rounded-full px-2 flex flex-col items-center"
|
||||
>
|
||||
{currentLocation}
|
||||
{currentLocation}.
|
||||
<MapCenterDetails {state}/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||
import DirectionIndicator from "../Base/DirectionIndicator.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let feature: Feature
|
||||
|
@ -14,11 +15,14 @@
|
|||
|
||||
</script>
|
||||
|
||||
<a class="small flex space-x-1 cursor-pointer w-fit" href={`#${feature.properties.id}`}>
|
||||
<span class="inline-flex gap-x-1">
|
||||
|
||||
<a class="small flex space-x-0.5 cursor-pointer w-fit items-center" href={`#${feature.properties.id}`}>
|
||||
{#if i !== undefined}
|
||||
<span class="font-bold">{i + 1} </span>
|
||||
{/if}
|
||||
<TagRenderingAnswer config={layer.title} extraClasses="inline-flex w-fit" {layer} selectedElement={feature} {state}
|
||||
{tags} />
|
||||
<DirectionIndicator {feature} {state} />
|
||||
</a>
|
||||
<DirectionIndicator {feature} {state} />
|
||||
</span>
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
lastAction.stabilized(750).addCallbackAndRunD((_) => lastAction.setData(undefined))
|
||||
</script>
|
||||
|
||||
<div aria-live="assertive" class="p-1 interactive" role="alert">
|
||||
<div aria-live="assertive" class="p-1 bg-white m-1 rounded">
|
||||
{#if $lastAction?.key === "out"}
|
||||
<Tr t={t.out.Subs({z: map.zoom.data - 1})} />
|
||||
{:else if $lastAction?.key === "in"}
|
||||
|
@ -46,13 +46,11 @@
|
|||
<div class="pointer-events-auto">
|
||||
<Tr t={$translationWithLength} />
|
||||
<MapCenterDetails {state} />
|
||||
<ol>
|
||||
<div class="grid grid-cols-3 space-x-1 space-y-0.5">
|
||||
{#each $centerFeatures.slice(0, 9) as feat, i (feat.properties.id)}
|
||||
<li>
|
||||
<Summary {state} feature={feat} {i} />
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte"
|
||||
import type { Feature } from "geojson"
|
||||
import { ImmutableStore, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import Center from "../../assets/svg/Center.svelte"
|
||||
import DirectionIndicator from "../Base/DirectionIndicator.svelte"
|
||||
|
||||
export let feature: Feature
|
||||
let properties: Record<string, string> = feature.properties
|
||||
|
@ -30,11 +30,6 @@
|
|||
center()
|
||||
}
|
||||
|
||||
const coord = GeoOperations.centerpointCoordinates(feature)
|
||||
const distance = state.mapProperties.location.stabilized(500).mapD(({ lon, lat }) => {
|
||||
let meters = Math.round(GeoOperations.distanceBetween(coord, [lon, lat]))
|
||||
return GeoOperations.distanceToHuman(meters)
|
||||
})
|
||||
const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
|
||||
</script>
|
||||
|
||||
|
@ -71,12 +66,7 @@
|
|||
{/if}
|
||||
{/each}
|
||||
|
||||
<button class="p-1" on:click={() => center()}>
|
||||
<Center class="h-6 w-6" />
|
||||
</button>
|
||||
<div class="w-14">
|
||||
{$distance}
|
||||
</div>
|
||||
<DirectionIndicator {state} {feature} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
|
||||
<Tr t={Translations.t.favouritePoi.intro.Subs({ length: $favourites?.length ?? 0 })} />
|
||||
<Tr t={Translations.t.favouritePoi.privacy} />
|
||||
<Tr t={Translations.t.favouritePoi.priintroPrivacyvacy} />
|
||||
|
||||
{#each $favourites as feature (feature.properties.id)}
|
||||
<FavouriteSummary {feature} {state} />
|
||||
|
|
|
@ -57,7 +57,11 @@
|
|||
validator = Validators.get(type ?? "string")
|
||||
|
||||
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
|
||||
if(_value.data?.length > 0){
|
||||
feedback?.setData(validator?.getFeedback(_value.data, getCountry))
|
||||
}else{
|
||||
feedback?.setData(undefined)
|
||||
}
|
||||
|
||||
initValueAndDenom()
|
||||
}
|
||||
|
@ -65,9 +69,14 @@
|
|||
function setValues() {
|
||||
// Update the value stores
|
||||
const v = _value.data
|
||||
if (!validator?.isValid(v, getCountry) || v === "") {
|
||||
if(v === ""){
|
||||
value.setData(undefined)
|
||||
feedback.setData(undefined)
|
||||
return
|
||||
}
|
||||
if (!validator?.isValid(v, getCountry)) {
|
||||
feedback?.setData(validator?.getFeedback(v, getCountry))
|
||||
value.setData("")
|
||||
value.setData(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -16,15 +16,22 @@ export default class NatValidator extends IntValidator {
|
|||
return str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* const validator = new NatValidator()
|
||||
* validator.getFeedback(-4).textFor("en") // => "This number should be positive"
|
||||
*/
|
||||
getFeedback(s: string): Translation {
|
||||
console.log("Getting feedback for", s)
|
||||
const n = Number(s)
|
||||
if (!isNaN(n) && n < 0) {
|
||||
return Translations.t.validation.nat.mustBePositive
|
||||
}
|
||||
const spr = super.getFeedback(s)
|
||||
if (spr !== undefined) {
|
||||
return spr
|
||||
}
|
||||
const n = Number(s)
|
||||
if (n < 0) {
|
||||
return Translations.t.validation.nat.mustBePositive
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,16 +3,20 @@
|
|||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import DynamicIcon from "./DynamicIcon.svelte"
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
|
||||
import { Orientation } from "../../Sensors/Orientation"
|
||||
|
||||
/**
|
||||
* Renders a 'marker', which consists of multiple 'icons'
|
||||
*/
|
||||
export let marker: IconConfig[] = config?.marker
|
||||
export let marker: IconConfig[]
|
||||
export let tags: Store<Record<string, string>>
|
||||
export let rotation: TagRenderingConfig = undefined
|
||||
let _rotation = rotation
|
||||
let _rotation: Store<string> = rotation
|
||||
? tags.map((tags) => rotation.GetRenderValue(tags).Subs(tags).txt)
|
||||
: new ImmutableStore(0)
|
||||
: new ImmutableStore("0deg")
|
||||
if(rotation?.render?.txt === "{alpha}deg"){
|
||||
_rotation = Orientation.singleton.alpha.map(alpha => alpha ? (alpha)+"deg" : "0deg ")
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if marker && marker}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
import Confirm from "../../assets/svg/Confirm.svelte"
|
||||
import Not_found from "../../assets/svg/Not_found.svelte"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import Direction_gradient from "../../assets/svg/Direction_gradient.svelte"
|
||||
|
||||
/**
|
||||
* Renders a single icon.
|
||||
|
@ -100,6 +101,8 @@
|
|||
<HeartOutlineIcon class={clss} />
|
||||
{:else if icon === "confirm"}
|
||||
<Confirm class={clss} {color} />
|
||||
{:else if icon === "direction"}
|
||||
<Direction_gradient class={clss} {color} />
|
||||
{:else if icon === "not_found"}
|
||||
<Not_found class={twMerge(clss, "no-image-background")} {color} />
|
||||
{:else}
|
||||
|
|
|
@ -171,6 +171,7 @@ class PointRenderingLayer {
|
|||
store
|
||||
.map((tags) => this._config.rotationAlignment.GetRenderValue(tags).Subs(tags).txt)
|
||||
.addCallbackAndRun((pitchAligment) => marker.setRotationAlignment(<any>pitchAligment))
|
||||
|
||||
if (feature.geometry.type === "Point") {
|
||||
// When the tags get 'pinged', check that the location didn't change
|
||||
store.addCallbackAndRunD(() => {
|
||||
|
|
|
@ -4,13 +4,11 @@
|
|||
import StarsBar from "./StarsBar.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
|
||||
export let review: Review & { madeByLoggedInUser: Store<boolean> }
|
||||
export let review: Review & { kid: string,signature: string, madeByLoggedInUser: Store<boolean> }
|
||||
let name = review.metadata.nickname
|
||||
name ??= (review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "").trim()
|
||||
if (name.length === 0) {
|
||||
name = "Anonymous"
|
||||
}
|
||||
name ??= ((review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "")).trim()
|
||||
let d = new Date()
|
||||
d.setTime(review.iat * 1000)
|
||||
let date = d.toDateString()
|
||||
|
@ -19,18 +17,32 @@
|
|||
|
||||
<div class={"low-interaction rounded-lg p-1 px-2 " + ($byLoggedInUser ? "border-interactive" : "")}>
|
||||
<div class="flex items-center justify-between">
|
||||
<StarsBar score={review.rating} />
|
||||
<div class="flex flex-wrap space-x-2">
|
||||
<div class="font-bold">
|
||||
{name}
|
||||
<div tabindex="0" use:ariaLabel={Translations.t.reviews.rated.Subs({n: ""+(Math.round(review.rating / 10)/2)})}>
|
||||
<StarsBar readonly={true} score={review.rating} />
|
||||
</div>
|
||||
<div class="flex flex-wrap space-x-2">
|
||||
<a href={`https://mangrove.reviews/list?kid=${encodeURIComponent(review.kid)}`} rel="noopener"
|
||||
target="_blank">
|
||||
{#if !name}
|
||||
<i>Anonymous</i>
|
||||
{:else}
|
||||
<span class="font-bold">
|
||||
{name}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
<span class="subtle">
|
||||
{date}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{#if review.opinion}
|
||||
<div class="link-no-underline">
|
||||
<a target="_blank" rel="noopener nofollow"
|
||||
href={`https://mangrove.reviews/list?signature=${encodeURIComponent(review.signature)}`}>
|
||||
{review.opinion}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
{#if review.metadata.is_affiliated}
|
||||
<Tr t={Translations.t.reviews.affiliated_reviewer_warning} />
|
||||
|
|
|
@ -1,27 +1,45 @@
|
|||
<script lang="ts">
|
||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import Star from "../../assets/svg/Star.svelte"
|
||||
import Star_half from "../../assets/svg/Star_half.svelte"
|
||||
import Star_outline from "../../assets/svg/Star_outline.svelte"
|
||||
import { ariaLabel, ariaLabelStore } from "../../Utils/ariaLabel"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
export let score: number
|
||||
export let cutoff: number
|
||||
export let starSize = "w-h h-4"
|
||||
export let i: number
|
||||
export let readonly = false
|
||||
|
||||
let dispatch = createEventDispatcher<{ hover: { score: number }, click: { score: number } }>()
|
||||
let container: HTMLElement
|
||||
|
||||
function getScore(e: MouseEvent): number {
|
||||
if (e.clientX === 0 && e.clientY === 0) {
|
||||
// Keyboard triggered 'click' -> return max value
|
||||
return cutoff
|
||||
}
|
||||
const x = e.clientX - e.target.getBoundingClientRect().x
|
||||
const w = container.getClientRects()[0]?.width
|
||||
return x / w < 0.5 ? cutoff - 10 : cutoff
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
{#if readonly}
|
||||
{#if score >= cutoff}
|
||||
<Star class={starSize} />
|
||||
{:else if score + 10 >= cutoff}
|
||||
<Star_half class={starSize} />
|
||||
{:else}
|
||||
<Star_outline class={starSize} />
|
||||
{/if}
|
||||
|
||||
{:else}
|
||||
<button
|
||||
use:ariaLabel={Translations.t.reviews.rate.Subs({n: i+1})}
|
||||
class="small soft rounded-full no-image-background"
|
||||
style="padding: 0; border: none;"
|
||||
bind:this={container}
|
||||
on:click={(e) => dispatch("click", { score: getScore(e) })}
|
||||
on:mousemove={(e) => dispatch("hover", { score: getScore(e) })}
|
||||
|
@ -33,4 +51,5 @@
|
|||
{:else}
|
||||
<Star_outline class={starSize} />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import StarElement from "./StarElement.svelte"
|
||||
|
||||
/**
|
||||
|
@ -9,12 +8,13 @@
|
|||
|
||||
let cutoffs = [20, 40, 60, 80, 100]
|
||||
export let starSize = "w-h h-4"
|
||||
export let readonly = false
|
||||
</script>
|
||||
|
||||
{#if score !== undefined}
|
||||
<div class="flex" on:mouseout>
|
||||
{#each cutoffs as cutoff, i}
|
||||
<StarElement {score} {i} {cutoff} {starSize} on:hover on:click />
|
||||
<StarElement {readonly} {score} {i} {cutoff} {starSize} on:hover on:click />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import StarsBar from "./StarsBar.svelte"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
export let score: Store<number>
|
||||
let scoreRounded = score.mapD(count => Math.round(count / 10) / 2)
|
||||
</script>
|
||||
|
||||
{#if $score !== undefined && $score !== null}
|
||||
<StarsBar score={$score} />
|
||||
<div tabindex="0"
|
||||
use:ariaLabel={Translations.t.reviews.averageRating.Subs({n: $scoreRounded})}>
|
||||
<StarsBar readonly={true} score={$score} />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -19,6 +19,7 @@ import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager"
|
|||
import { OsmTags } from "../Models/OsmFeature"
|
||||
import FavouritesFeatureSource from "../Logic/FeatureSource/Sources/FavouritesFeatureSource"
|
||||
import { ProvidedImage } from "../Logic/ImageProviders/ImageProvider"
|
||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||
|
||||
/**
|
||||
* The state needed to render a special Visualisation.
|
||||
|
@ -87,6 +88,7 @@ export interface SpecialVisualizationState {
|
|||
readonly imageUploadManager: ImageUploadManager
|
||||
|
||||
readonly previewedImage: UIEventSource<ProvidedImage>
|
||||
readonly geolocation: GeoLocationHandler
|
||||
}
|
||||
|
||||
export interface SpecialVisualization {
|
||||
|
|
|
@ -85,6 +85,9 @@ import { Unit } from "../Models/Unit"
|
|||
import Link from "./Base/Link.svelte"
|
||||
import OrientationDebugPanel from "./Debug/OrientationDebugPanel.svelte"
|
||||
import MaprouletteSetStatus from "./MapRoulette/MaprouletteSetStatus.svelte"
|
||||
import DirectionIndicator from "./Base/DirectionIndicator.svelte"
|
||||
import Img from "./Base/Img"
|
||||
import Qr from "../Utils/Qr"
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
@ -1539,6 +1542,43 @@ export default class SpecialVisualizations {
|
|||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "direction_indicator",
|
||||
args: [],
|
||||
needsUrls: [],
|
||||
docs: "Gives a distance indicator and a compass pointing towards the location from your GPS-location. If clicked, centers the map on the object",
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
return new SvelteUIElement(DirectionIndicator, { state, feature })
|
||||
},
|
||||
},
|
||||
{
|
||||
funcName: "qr_code",
|
||||
args: [],
|
||||
needsUrls: [],
|
||||
docs: "Generates a QR-code to share the selected object",
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
const url =
|
||||
window.location.protocol +
|
||||
"//" +
|
||||
window.location.host +
|
||||
window.location.pathname +
|
||||
"#" +
|
||||
feature.properties.id
|
||||
return new Img(new Qr(url).toImageElement(75)).SetStyle("width: 75px")
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
specialVisualizations.push(new AutoApplyButton(specialVisualizations))
|
||||
|
|
|
@ -4,6 +4,9 @@ import BaseUIElement from "../BaseUIElement"
|
|||
import CompiledTranslations from "../../assets/generated/CompiledTranslations"
|
||||
import LanguageUtils from "../../Utils/LanguageUtils"
|
||||
import { ClickableToggle } from "../Input/Toggle"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Locale from "./Locale"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export default class Translations {
|
||||
static readonly t: Readonly<typeof CompiledTranslations.t> = CompiledTranslations.t
|
||||
|
@ -130,6 +133,29 @@ export default class Translations {
|
|||
}
|
||||
}
|
||||
|
||||
public static DynamicSubstitute<T extends Record<string, string | Translation>>(
|
||||
translation: TypedTranslation<T>,
|
||||
t: Store<T>
|
||||
): Store<string> {
|
||||
return Locale.language.map(
|
||||
(lang) => {
|
||||
const tags: Record<string, string> = {}
|
||||
for (const k in t.data) {
|
||||
let v = t.data[k]
|
||||
if (!v) {
|
||||
continue
|
||||
}
|
||||
if (v["textFor"] !== undefined) {
|
||||
v = v["textFor"](lang)
|
||||
}
|
||||
tags[k] = <string>v
|
||||
}
|
||||
return Utils.SubstituteKeys(translation.textFor(lang), t.data)
|
||||
},
|
||||
[t]
|
||||
)
|
||||
}
|
||||
|
||||
static isProbablyATranslation(transl: any) {
|
||||
if (!transl || typeof transl !== "object") {
|
||||
return false
|
||||
|
|
|
@ -4,23 +4,20 @@ import Qrcode from "qrcode-generator"
|
|||
* Creates a QR-code as Blob
|
||||
*/
|
||||
export default class Qr {
|
||||
private _textToShow: string
|
||||
private readonly _textToShow: string
|
||||
|
||||
constructor(textToShow: string) {
|
||||
this._textToShow = textToShow
|
||||
}
|
||||
|
||||
public toImageElement(totalSize: number): string {
|
||||
console.log("Creating a QR code for", this._textToShow)
|
||||
const typeNumber = 0
|
||||
const errorCorrectionLevel = "L"
|
||||
const qr = Qrcode(typeNumber, errorCorrectionLevel)
|
||||
qr.addData(this._textToShow)
|
||||
qr.make()
|
||||
const moduleCount = qr.getModuleCount()
|
||||
const img = document.createElement("img")
|
||||
const cellSize = Math.round(totalSize / moduleCount)
|
||||
console.log("Cellsize", cellSize)
|
||||
return qr.createDataURL(cellSize)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue