diff --git a/src/parking/controls/editor/editor-form.ts b/src/parking/controls/editor/editor-form.ts index f487819..072ac08 100644 --- a/src/parking/controls/editor/editor-form.ts +++ b/src/parking/controls/editor/editor-form.ts @@ -4,6 +4,7 @@ import { WaysInRelation } from '../../../utils/types/osm-data-storage' import { OsmKeyValue } from '../../../utils/types/preset' import { presets } from './presets' import { getAllTagsBlock } from '../lane-info' +import { parseConditionalTag, ConditionalValue } from '../../../utils/conditional-tag' export function getLaneEditForm(osm: OsmWay, waysInRelation: WaysInRelation, cutLaneListener: (way: OsmWay) => void): HTMLFormElement { const form = hyper` @@ -91,6 +92,7 @@ const parkingLaneTagTemplates = [ 'parking:condition:{side}', 'parking:condition:{side}:time_interval', 'parking:condition:{side}:default', + 'parking:condition:{side}:conditional', 'parking:condition:{side}:maxstay', 'parking:lane:{side}:capacity', 'parking:lane:{side}:surface', @@ -112,6 +114,13 @@ function getTagInput(osm: OsmWay, side: string, parkingType: string, tagTemplate const tagSplit = tag.split(':') const label = tagSplit[Math.floor(tagSplit.length / 2) * 2 - 1] + if (tagTemplate === 'parking:condition:{side}:conditional') { + const conditionTag = 'parking:condition:{side}' + .replace('{side}', side) + const hide = !osm.tags[conditionTag] + return getConditionalInput(osm, tag, label, hide) + } + const value = osm.tags[tag] let input: HTMLInputElement | HTMLSelectElement @@ -126,6 +135,7 @@ function getTagInput(osm: OsmWay, side: string, parkingType: string, tagTemplate case 'parking:condition:{side}:time_interval': input = getTextInput(tag, value) input.oninput = handleTimeIntervalTagInput + hide = !osm.tags[tagTemplate.replace('{side}', side)] break case 'parking:lane:{side}:{type}': { @@ -169,14 +179,7 @@ function getTagInput(osm: OsmWay, side: string, parkingType: string, tagTemplate break } - input.onchange = (e) => { - if (!(e.currentTarget instanceof HTMLInputElement || e.currentTarget instanceof HTMLSelectElement) || - e.currentTarget.form == null) - return - - const newOsm = formToOsmWay(osm, e.currentTarget.form) - osmChangeListener?.(newOsm) - } + input.onchange = (e) => handleInputChange(e, osm) return hyper` ` as HTMLElement } +function handleInputChange(e: Event, osm: OsmWay) { + if (!(e.currentTarget instanceof HTMLInputElement || e.currentTarget instanceof HTMLSelectElement) || + e.currentTarget.form == null) + return + + const newOsm = formToOsmWay(osm, e.currentTarget.form) + osmChangeListener?.(newOsm) +} + function getSelectInput(tag: string, value: string, values: string[]): HTMLSelectElement { const options = !value || values.includes(value) ? ['', ...values] : ['', value, ...values] return hyper` - ${options.map(o => hyper``)} ` } @@ -202,11 +215,52 @@ function getSelectInput(tag: string, value: string, values: string[]): HTMLSelec function getTextInput(tag: string, value: string): HTMLInputElement { return hyper` ` } +function getConditionalInput(osm: OsmWay, tag: string, label: string, hide: boolean): HTMLElement { + const parsedConditionalTag = osm.tags[tag] ? parseConditionalTag(osm.tags[tag]) : [] + parsedConditionalTag.push({ value: '', condition: null }) + + return hyper` + + + + + ${parsedConditionalTag.map((conditionalValue, i) => getConditionalPartInput(osm, tag, conditionalValue, i))} +
+ + ` +} + +function getConditionalPartInput(osm: OsmWay, tag: string, part: ConditionalValue, partindex: number) { + const selectInput = getSelectInput(`${tag}`, part.value, conditionValues) + selectInput.onchange = (e) => handleInputChange(e, osm) + selectInput.dataset.partindex = partindex.toString() + selectInput.dataset.tokenname = 'condition' + + return hyper` + + + ${selectInput} + + + handleInputChange(e, osm)}> + + ` +} + function getPresetSigns(osm: OsmWay, side: 'both'|'left'|'right') { return presets.map(x => hyper` void @@ -339,7 +400,7 @@ export function setOsmChangeListener(listener: (way: OsmWay) => void) { } function formToOsmWay(osm: OsmWay, form: HTMLFormElement) { - const regex = /^parking:/ + const regex = /^parking:(?!>conditional$)/ const supprtedTags = parkingLaneTagTemplates .map(x => { @@ -354,10 +415,32 @@ function formToOsmWay(osm: OsmWay, form: HTMLFormElement) { delete osm.tags[tagKey] } + const conditionals: {[tag: string]: string[][]} = {} + for (const input of Array.from(form.elements)) { - if ((input instanceof HTMLInputElement || input instanceof HTMLSelectElement) && - regex.test(input.name) && input.value) - osm.tags[input.name] = input.value + if (input instanceof HTMLInputElement || input instanceof HTMLSelectElement) { + if (regex.test(input.name) && input.value) + osm.tags[input.name] = input.value + + if (input.dataset.partindex) { + if (!conditionals[input.name]) + conditionals[input.name] = [] + + if (conditionals[input.name].length < parseInt(input.dataset.partindex) + 1) + conditionals[input.name].push(['', '']) + + conditionals[input.name][parseInt(input.dataset.partindex)][input.dataset.tokenname === 'condition' ? 0 : 1] = input.value + } + } + } + + for (const conditionalTag in conditionals) { + if (conditionals[conditionalTag].length > 0 && conditionals[conditionalTag][0][0]) { + osm.tags[conditionalTag] = conditionals[conditionalTag] + .filter(x => x[0]) + .map(x => x[1] ? `${x[0]} @ (${x[1]})` : x[0]) + .join('; ') + } } return osm diff --git a/src/parking/interface.ts b/src/parking/interface.ts index 30c2fd2..ca1a5d1 100644 --- a/src/parking/interface.ts +++ b/src/parking/interface.ts @@ -39,7 +39,7 @@ import { ParkingAreas, ParkingLanes } from '../utils/types/parking' import { parseParkingArea, updateAreaColorsByDate } from './parking-area' const editorName = 'PLanes' -const version = '0.6.1' +const version = '0.7.0' let editorMode = false const useDevServer = false diff --git a/src/parking/parking-area.ts b/src/parking/parking-area.ts index 4c1277d..151045b 100644 --- a/src/parking/parking-area.ts +++ b/src/parking/parking-area.ts @@ -1,6 +1,6 @@ import L from 'leaflet' import { getOpeningHourseState, parseOpeningHourse } from '../utils/opening-hours' -import { ConditionColor, ConditionsInterface } from '../utils/types/conditions' +import { ConditionColor, ParkingConditions } from '../utils/types/conditions' import { ParkingPolylineOptions } from '../utils/types/leaflet' import { OsmTags, OsmWay } from '../utils/types/osm-data' import { ParkingAreas } from '../utils/types/parking' @@ -24,30 +24,30 @@ export function parseParkingArea( } function getConditions(tags: OsmTags) { - const conditions: ConditionsInterface = { - intervals: [], + const conditions: ParkingConditions = { + conditionalValues: [], default: getDefaultCondition(tags), } if (tags.opening_hours) { - conditions.intervals!.push({ - interval: parseOpeningHourse(tags.opening_hours), - condition: conditions.default!, + conditions.conditionalValues!.push({ + condition: parseOpeningHourse(tags.opening_hours), + parkingCondition: conditions.default!, }) conditions.default = 'no_stopping' } if (tags.fee && tags.fee !== 'yes' && tags.fee !== 'no') { - conditions.intervals?.push({ - interval: parseOpeningHourse(tags.fee), - condition: 'ticket', + conditions.conditionalValues?.push({ + condition: parseOpeningHourse(tags.fee), + parkingCondition: 'ticket', }) } if (tags['fee:conditional']) { const match = tags['fee:conditional'].match(/(?.*?) *@ *\((?.*?)\)/) if (match?.groups?.interval) { - conditions.intervals?.push({ - interval: parseOpeningHourse(match?.groups?.interval), - condition: match?.groups?.value === 'yes' ? 'ticket' : 'free', + conditions.conditionalValues?.push({ + condition: parseOpeningHourse(match?.groups?.interval), + parkingCondition: match?.groups?.value === 'yes' ? 'ticket' : 'free', }) } } @@ -84,7 +84,7 @@ function getDefaultCondition(tags: OsmTags) { } } -function createPolygon(line: L.LatLngLiteral[], conditions: ConditionsInterface | undefined, osm: OsmWay, zoom: number) { +function createPolygon(line: L.LatLngLiteral[], conditions: ParkingConditions | undefined, osm: OsmWay, zoom: number) { const polylineOptions: ParkingPolylineOptions = { color: getColor(conditions?.default), fillOpacity: 0.6, @@ -113,14 +113,14 @@ export function updateAreaColorsByDate(areas: ParkingAreas, datetime: Date): voi } } -function getColorByDate(conditions: ConditionsInterface, datetime: Date): ConditionColor | undefined { +function getColorByDate(conditions: ParkingConditions, datetime: Date): ConditionColor | undefined { if (!conditions) return 'black' // If conditions.intervals not defined, return the default color - for (const interval of conditions.intervals ?? []) { - if (interval.interval && getOpeningHourseState(interval.interval, datetime)) - return getColor(interval.condition) + for (const interval of conditions.conditionalValues ?? []) { + if (interval.condition && getOpeningHourseState(interval.condition, datetime)) + return getColor(interval.parkingCondition) } return getColor(conditions.default) } diff --git a/src/parking/parking-lane.ts b/src/parking/parking-lane.ts index 6c6a769..8593d24 100644 --- a/src/parking/parking-lane.ts +++ b/src/parking/parking-lane.ts @@ -3,7 +3,7 @@ import { parseOpeningHourse, getOpeningHourseState } from '../utils/opening-hour import { legend } from './legend' import { laneStyleByZoom as laneStyle } from './lane-styles' -import { ConditionColor, ConditionInterface, ConditionsInterface } from '../utils/types/conditions' +import { ConditionColor, ConditionalParkingCondition, ParkingConditions } from '../utils/types/conditions' import { OsmWay, OsmTags } from '../utils/types/osm-data' import { ParkingLanes, Side } from '../utils/types/parking' import { ParkingPolylineOptions } from '../utils/types/leaflet' @@ -31,7 +31,7 @@ export function parseParkingLane( for (const side of ['right', 'left'] as Side[]) { const conditions = getConditions(side, way.tags) - if (conditions.default != null || (conditions.intervals && conditions.intervals.length > 0)) { + if (conditions.default != null || (conditions.conditionalValues && conditions.conditionalValues.length > 0)) { const laneId = generateLaneId(way, side, conditions) const offset: number = isMajor ? laneStyle[zoom].offsetMajor as number : @@ -97,14 +97,14 @@ export function parseChangedParkingLane(newOsm: OsmWay, lanes: ParkingLanes, dat return newLanes } -function generateLaneId(osm: OsmWay, side?: 'left' | 'right', conditions?: ConditionsInterface) { +function generateLaneId(osm: OsmWay, side?: 'left' | 'right', conditions?: ParkingConditions) { if (!conditions) return 'empty' + osm.id return side! + osm.id } -function createPolyline(line: L.LatLngLiteral[], conditions: ConditionsInterface | undefined, side: string, osm: OsmWay, offset: number, isMajor: boolean, zoom: number) { +function createPolyline(line: L.LatLngLiteral[], conditions: ParkingConditions | undefined, side: string, osm: OsmWay, offset: number, isMajor: boolean, zoom: number) { const polylineOptions: ParkingPolylineOptions = { color: getColor(conditions?.default), weight: isMajor ? laneStyle[zoom].weightMajor : laneStyle[zoom].weightMinor, @@ -130,15 +130,15 @@ function wayIsMajor(tags: OsmTags) { return tags.highway.search(majorHighwayRegex) >= 0 } -function getConditions(side: 'left' | 'right', tags: OsmTags): ConditionsInterface { - const conditions: ConditionsInterface = { intervals: [], default: null } +function getConditions(side: 'left' | 'right', tags: OsmTags): ParkingConditions { + const conditions: ParkingConditions = { conditionalValues: [], default: null } - conditions.intervals = parseConditionsByNewScheme(side, tags) - if (conditions.intervals.length > 0) { + conditions.conditionalValues = parseConditionsByNewScheme(side, tags) + if (conditions.conditionalValues.length > 0) { conditions.default = parseDefaultCondition(side, tags, 0) } else { - conditions.intervals = parseConditionsByOldScheme(side, tags) - conditions.default = parseDefaultCondition(side, tags, conditions.intervals.length) + conditions.conditionalValues = parseConditionsByOldScheme(side, tags) + conditions.default = parseDefaultCondition(side, tags, conditions.conditionalValues.length) } return conditions } @@ -170,22 +170,24 @@ function parseDefaultCondition(side: string, tags: OsmTags, findedByOldSchemeInt } function parseConditionsByNewScheme(side: string, tags: OsmTags) { - const conditionalTag = [side, 'both'].map(side => 'parking:condition:' + side + ':conditional').find(tag => tags[tag]) + const conditionalTag = [side, 'both'] + .map(side => 'parking:condition:' + side + ':conditional') + .find(tag => tags[tag]) if (!conditionalTag) return [] - const intervals: ConditionInterface[] = parseConditionalTag(tags[conditionalTag]) + const intervals: ConditionalParkingCondition[] = parseConditionalTag(tags[conditionalTag]) .map(x => ({ - condition: x[0], - interval: parseOpeningHourse(x[1]), + parkingCondition: x.value, + condition: parseOpeningHourse(x.condition), })) return intervals } function parseConditionsByOldScheme(side: string, tags: OsmTags) { - const intervals: ConditionInterface[] = [] + const conditionalParkingConditions: ConditionalParkingCondition[] = [] const sides = ['both', side] for (let i = 1; i < 10; i++) { @@ -195,32 +197,32 @@ function parseConditionsByOldScheme(side: string, tags: OsmTags) { const conditionTags = sides.map(side => 'parking:condition:' + side + index) const intervalTags = sides.map(side => 'parking:condition:' + side + index + ':time_interval') - const cond: ConditionInterface = { condition: null, interval: null } + const conditionalParkingCondition: ConditionalParkingCondition = { parkingCondition: null, condition: null } for (let j = 0; j < sides.length; j++) { let tagValue = tags[laneTags[j]] if (tagValue && legend.findIndex(x => x.condition === tagValue) >= 0) - cond.condition = tagValue + conditionalParkingCondition.parkingCondition = tagValue tagValue = tags[conditionTags[j]] if (tagValue) - cond.condition = tagValue + conditionalParkingCondition.parkingCondition = tagValue tagValue = tags[intervalTags[j]] if (tagValue) - cond.interval = parseOpeningHourse(tagValue) + conditionalParkingCondition.condition = parseOpeningHourse(tagValue) } - if (i === 1 && cond.interval == null) + if (i === 1 && conditionalParkingCondition.condition == null) break - if (cond.condition) - intervals?.push(cond) + if (conditionalParkingCondition.parkingCondition) + conditionalParkingConditions?.push(conditionalParkingCondition) else break } - return intervals + return conditionalParkingConditions } /** The time effects the current parking restrictions. Update colors based on this. */ @@ -231,16 +233,16 @@ export function updateLaneColorsByDate(lanes: ParkingLanes, datetime: Date): voi } } -function getColorByDate(conditions: ConditionsInterface, datetime: Date): ConditionColor | undefined { - if (!conditions) +function getColorByDate(parkingConditions: ParkingConditions, datetime: Date): ConditionColor | undefined { + if (!parkingConditions) return 'black' // If conditions.intervals not defined, return the default color - for (const interval of conditions.intervals ?? []) { - if (interval.interval && getOpeningHourseState(interval.interval, datetime)) - return getColor(interval.condition) + for (const conditionalValue of parkingConditions.conditionalValues ?? []) { + if (conditionalValue.condition && getOpeningHourseState(conditionalValue.condition, datetime)) + return getColor(conditionalValue.parkingCondition) } - return getColor(conditions.default) + return getColor(parkingConditions.default) } export function updateLaneStylesByZoom(lanes: ParkingLanes, zoom: number): void { diff --git a/src/styles/main.scss b/src/styles/main.scss index a830093..72a1e0f 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -46,12 +46,6 @@ input[type="datetime-local"]::-webkit-calendar-picker-indicator { margin: 0; } -select, -input[type=text] { - width: 100%; - box-sizing: border-box; -} - .leaflet-marker-icon.cut-icon { background: #fffc7e; color: black; @@ -135,11 +129,23 @@ input[type=text] { background: #f4f4f4; } +.editor-form__select-input { + width: 100%; +} + .sign-preset { margin: 2px; cursor: pointer; } +.conditional-tag > td { + padding: 0; +} + +.conditional-tag table { + border-spacing: 0; +} + // github control .leaflet-control-layers .editor-mode, diff --git a/src/utils/conditional-tag.ts b/src/utils/conditional-tag.ts index 69a351c..10dfb82 100644 --- a/src/utils/conditional-tag.ts +++ b/src/utils/conditional-tag.ts @@ -1,7 +1,7 @@ export function parseConditionalTag(tag: string) { const bracketStack: number[] = [] let prevConditionEndPosition: number | null = null - const conditions: string[][] = [] + const parsedConditionalTag: ConditionalValue[] = [] for (let i = 0; i < tag.length; i++) { const char = tag.charAt(i) @@ -18,29 +18,39 @@ export function parseConditionalTag(tag: string) { case ';': { if (bracketStack.length === 0) { const startPosition = prevConditionEndPosition ? prevConditionEndPosition + 1 : 0 - conditions.push(getConditionalTagPart(tag, startPosition, i)) + parsedConditionalTag.push(parseConditionalValue(tag.substring(startPosition, i - 1))) prevConditionEndPosition = i } } } } - if (prevConditionEndPosition == null || prevConditionEndPosition < tag.length - 2) { + if (prevConditionEndPosition == null || prevConditionEndPosition < tag.length - (tag.endsWith(')') ? 2 : 1)) { const startPosition = prevConditionEndPosition ? prevConditionEndPosition + 1 : 0 - conditions.push(getConditionalTagPart(tag, startPosition, tag.length)) + parsedConditionalTag.push(parseConditionalValue(tag.substring(startPosition, tag.length - (tag.endsWith(')') ? 1 : 0)))) } - return conditions + return parsedConditionalTag } -function getConditionalTagPart(tag: string, start: number, end: number) { - const condition = tag.substring(start, end).split('@', 2) - condition[0] = condition[0].trim() - if (condition.length > 1) { - condition[1] = condition[1].trim() - condition[1] = condition[1].substring(1, condition[1].length - 1) - } else { - condition.push('') +function parseConditionalValue(rawConditionalValue: string) { + const tokens = rawConditionalValue.split('@', 2) + + const conditionalValue: ConditionalValue = { + value: tokens[0].trim(), + condition: null, + } + + if (tokens.length > 1) { + conditionalValue.condition = tokens[1] + .trim() + .substring(1, tokens[1].length - 1) } - return condition + + return conditionalValue +} + +export interface ConditionalValue { + value: string + condition: string | null } diff --git a/src/utils/opening-hours.ts b/src/utils/opening-hours.ts index 60dbc4e..c70bcb8 100644 --- a/src/utils/opening-hours.ts +++ b/src/utils/opening-hours.ts @@ -1,6 +1,9 @@ import OpeningHours from 'opening_hours' -export function parseOpeningHourse(value: string): OpeningHours | 'even' | 'odd' | null { +export function parseOpeningHourse(value: string | null): OpeningHours | 'even' | 'odd' | null { + if (value == null) + return null + if (/\d+-\d+\/\d+$/.test(value)) { // @ts-expect-error return parseInt(value.match(/\d+/g)[0]) % 2 === 0 ? diff --git a/src/utils/types/conditions.ts b/src/utils/types/conditions.ts index b31e47b..8c07128 100644 --- a/src/utils/types/conditions.ts +++ b/src/utils/types/conditions.ts @@ -1,13 +1,13 @@ import OpeningHours from 'opening_hours' -export interface ConditionsInterface { - intervals?: ConditionInterface[] - default?: null | string // TODO add type: Can be free, no_parking, no_stopping and likely others +export interface ParkingConditions { + default?: string | null + conditionalValues?: ConditionalParkingCondition[] } -export interface ConditionInterface { - interval: OpeningHours | 'even' | 'odd' | null - condition: string | null +export interface ConditionalParkingCondition { + parkingCondition: string | null + condition: OpeningHours | 'even' | 'odd' | null } export type ConditionName = 'disc' | 'no_parking' | 'no_stopping' | 'free' | 'ticket' |'customers' | 'residents' | 'disabled' | 'disc' | 'no' | 'separate' | 'unsupported' diff --git a/src/utils/types/leaflet.ts b/src/utils/types/leaflet.ts index 515dba2..f956239 100644 --- a/src/utils/types/leaflet.ts +++ b/src/utils/types/leaflet.ts @@ -1,10 +1,10 @@ import L, { PolylineOptions } from 'leaflet' -import { ConditionsInterface } from './conditions' +import { ParkingConditions } from './conditions' import { OsmWay } from './osm-data' export interface ParkingPolylineOptions extends PolylineOptions { offset?: number | undefined - conditions?: ConditionsInterface + conditions?: ParkingConditions osm: OsmWay isMajor: boolean }